自然言語と人工言語のはざまで―ことばの研究・教育での言語処理技術の利用―(ことば文化特設サイト)
ことば文化に関する気になるトピックを短期連載で紹介していきます。
-
- 2023年03月28日 『12 自然言語と人工言語のはざまで―ことばの研究・教育での言語処理技術の利用―:大規模言語モデルとAPI 野口大斗(東京医科歯科大学ほか非常勤講師)』
-
大規模言語モデルとAPI
ChatGPTのAPI
いよいよ最終回となりました。今回は大規模言語モデルAPIの紹介と、今後の言語モデルとの付き合い方で連載を締めくくります。さっそく本題に入りましょう。
大規模言語モデル(Large Language Model)とは、膨大な言語データのパターンや関係性を学習し、その知識を用いて言語タスクをこなすことができるモデルのことです。現在、主流になっているものはOpenAIのシリーズのものです。この連載の初回にChatGPTについて触れましたが、まさにそれがその代表例です。今回はそのAPI(Application Programming Interface)を使用してみます。APIについては以前簡単に触れましたが、ソフトウェア同士でやり取りをするためのインターフェースです。つまり、スクリプトからGPTのモデルにアクセスします。
知識がなくても視覚的に操作できるChatGPTという無料のGUI(Graphical User Interface)があるのに、どうしてわざわざ課金してスクリプトを書いてCUI(Character User Interface)で操作する方法を説明するのかと怪訝に思われるかもしれません。主な理由としてはデータの保護と利便性の向上です。
まず、データが保護されることについてです。執筆時点において、API経由の入力データはモデルを改良するためのトレーニングデータとして使用されないことが明記されています。また、最大で30日間が経過したのちに破棄されることも約束されています。このことは入力できるデータの幅が広がり、研究機関や学校、企業などでも活用が可能になることを意味しています。考えてみればChatGPTに限らず当たり前のことですが、開発費のかかるシステムを無料で使わせてくれるのは、宣伝やデータ収集のためなのです。比較的安い対価を支払うことによって(https://openai.com/pricing)、自らの入力するデータを保護してもらうことができるのです(https://openai.com/policies/api-data-usage-policies)。Starting on March 1, 2023, we are making two changes to our data usage and retention policies:
利便性の向上についてですが、まとめてデータを処理したい場合にAPIを使用すると自動化でき、1つずつ手作業でユーザインタフェースに入力する手間が省けます。今回はPython経由のAPIの利用方法を紹介しますが、これまで紹介してきたようなスクリプトや自作のアプリケーションに組み込むことだってできるようになるのです。サービスを消費する側から作り出す側に近づくことができます。
OpenAI will not use data submitted by customers via our API to train or improve our models, unless you explicitly decide to share your data with us for this purpose. You can opt-in to share data.
Any data sent through the API will be retained for abuse and misuse monitoring purposes for a maximum of 30 days, after which it will be deleted (unless otherwise required by law).
さて、実際に使ってみることにしましょう。OpenAIが公開しているAPIは公式サイト(https://platform.openai.com)から確認することができます。GPT-4は現在、ウェイティングリストに登録して待たないと使用できるようにならないので、今回はgpt-3.5-turboを利用します。現状では、利用料金が1000トークンあたり0.002ドルとかなり手頃な価格になっています。執筆時点においては、フリートライアルも実施中ですので、各自でご確認ください。英語のトークンは多くの場合単語単位になりますが、日本語の場合は1文字で最大3トークンの扱いになることがあるようです。利用料金の上限を設定しておくとさらに安心です(https://platform.openai.com/account/billing/limits)。
まずは、OpenAIのアカウントが必要になります。すでに、ChatGPTを利用したことがあれば、ログインするだけで問題ありません(https://platform.openai.com)。APIキーを発行して、コピーしておきましょう(https://platform.openai.com/account/api-keys)。キーは他人の目に触れないように、取り扱いには十分に注意してください。安価とはいえ、流出して不特定多数の人に利用されてしまうと莫大な請求額になることがあります。支払い情報を登録すると(https://platform.openai.com/account/billing/overview)、すぐに使用できるようになります。
APIの利用コード
以下のコードのAPI_KEYの部分を、さきほどコピーしたAPIキーに置き換えて実行するだけです。これで、ChatGPTに“Hi! Can you tell me about yourself?”と入力したのと同じになります。どけち精神(経費節減)と回答の精度の問題で英語を使いますが、日本語でも問題ありません。要旨は著者がつけたものです。
答えの部分のみを抽出したい場合は、以下のコードを実行します。!pip install openai
import openai
openai.api_key = "API_KEY"
response1 = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{"role": "user", "content": "Hi! Can you tell me about yourself?"}
]
)
response1
ちなみに自己紹介してもらった結果も掲載しておきましょう。response1["choices"][0]["message"]["content"]
As an AI language model, I don't have a personal life like humans. But I can tell you that I'm here to help you with any questions or tasks you may have!
もうすこし食い下がってみましょう。会話の流れを理解させたい場合は、つぎのようにそれまでのやりとりをコンマでつなげてやります。自分の入力はroleをuserに、モデルの出力はassistantにします。つなげる会話が増えるとトークン数も増えるので、料金には注意しましょう。
(言語モデルのため、人間のような私生活はないんだ。でも、タスクや質問は手伝ってあげられるよ。)
ちなみに追加したのは一番下のmessageです。response2 = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{"role": "user", "content": "Hi! Can you tell me about yourself?"},
{"role": "assistant", "content": response1["choices"][0]["message"]["content"]},
{"role": "user", "content": "Even though you are a language model, you should be able to describe yourself to some extent, such as by explaining how intelligent you are or what tasks you can perform."}
]
)
response2["choices"][0]["message"]["content"]
Even though you are a language model, you should be able to describe yourself to some extent, such as by explaining how intelligent you are or what tasks you can perform.
(言語モデルでも何ができるか説明ぐらいできるんちゃうの。どれぐらい賢いかとか何ができるかとか…)
返答は以下のものでした。That's correct. As an AI language model, I have been designed to understand and respond to natural language queries and generate human-like text. I am constantly learning and improving my abilities to better assist people with various tasks, such as providing information on a wide range of topics, carrying out simple tasks like setting reminders or alarms, and even carrying out complex tasks such as automated customer support or data analysis. In terms of my intelligence, I rely on my extensive knowledge base, advanced algorithms, and machine learning capabilities to provide accurate and helpful responses to users.
あと1往復だけ続けてみましょう。こちらでのコードは省略しますが、“That is amazing. How can you help us make our lives better?”(すごいやん。どうやってええ生活するの手伝ってくれんの。)と聞いてみました。
(そうだね。自然言語を理解して、応答し、人間みたいな文を作れるよ。シンプルなタスクでも複雑なタスクでもこなせるように勉強しているんだ。豊富な知識と高度な技術で、正確かつ役に立つ情報を提供するよ。)As an AI language model, my main purpose is to make people's lives easier and more convenient by providing timely and accurate information and support. I can help you with a wide range of tasks, such as finding answers to your questions about current events, science, technology, and other topics of interest. I can also assist with tasks like setting reminders, sending messages, making appointments, and keeping track of your schedule. Additionally, I can assist with more complex tasks like customer support, data analysis, and automating various workflows. By leveraging my abilities, you can save time, improve your productivity, and accomplish your goals more efficiently.
もちろん返答は実行のたびに変わります。APIでは細かいパラメーターを設定できます。たとえば、temperature(サンプリング温度)を設定することで多様性を制限したり、nで返答の数を指定したり、max_tokensで出力の最大トークン数を入力したりできます。Temperatureを0に設定すると、実行のたびに答えが変わることはなくなります。
(生活をより楽で便利にできるよ。質問に答えることも、タスクの補助をすることもできるんだ。時間を節約して、生産性を上げて、目標をより効率的に達成できるようになるよ。)
言語モデルが教えてくれること
さて、これで連載の技術的な部分はすべて終わりです。わたしたちがこれから言語モデルとどう付き合えるのかについて、すこし私見をのべて終わりにします。
さきほどの回答に“By leveraging my abilities, you can save time, improve your productivity, and accomplish your goals more efficiently”とありましたが、この部分が言語モデルをめぐる、問題の核心をついています。連載の冒頭でも述べましたが、自動化自体は悪くないのです。言語の処理を自動化できるようになったことによって、何をすべきかというゴールや本質がより見えやすくなったという点がもっと注目されてよいはずです。
自動化において失われる作業とは、何かを達成するために副次的に必要な作業であり、その作業が目的そのものではありません。たとえば、会社の社長は、事務作業を事務員に、運転を運転手に、外国語を翻訳者や通訳者に委託することで、自分以外の人に解決させてきました。自動化が進むと、その分野で働く人々は失業する可能性があるものの、それはその作業から解放されることも意味します。そして、従来の社長のような資本がなくても、自動化でアイデアを形にできるようになることがあるかもしれません。つまり、自動化は機会を生みだしているのです。
言語の問題も同じはずです。研究者の例を考えてみましょう。たとえば、申請書や報告書を書く作業はもちろん、論文を読んだり書いたりする時間を削減することができます。論文を読んだり書いたりすることは、先行研究を知ったり研究結果を報告したりする手段のはずです。より研究自体に時間の割くことができるようになったと、よろこぶべきではないでしょうか。
言語モデルがある前提での生活が続く以上、失うものを嘆くのではなく、何を得られるのかを考えてみませんか。時間を有効に使い、生産性を上げ、ゴールに効率的にたどり着けるようになったのです。連載に関しても、残念ながらすべてChatGPTが書きましたというおちはないのですが、後半のいくつかの長いコードの下書きを手伝ってくれました。また、プログラミングにフォーカスするという計画を台無しにして、よりメッセージに注力させてくれました。口先だけではなく身をもって進むべき道を教えてくれました。
自然言語と人工言語のはざまで(1),(2)
もともとこのタイトルは、言語に興味はあるものの従来の言語学の研究に親しみを持てず、かといって人工言語で開発をする側のコミュニティーにも属さないわたしの立ち位置を示すものでした。しかし、ChatGPTの登場により期せずして、全人類が自然言語と人工言語のはざまをさまようこととなりました。連載は終わりますが、そのはざまをさまよう旅はまだ始まったばかりです。
注
(1) 共著でオンライン実験に関する書籍も鋭意執筆中です。夏ごろまでに教養検定会議から出版予定ですので、興味があればご覧ください。
(2) 大学院生のころに自然言語処理とPythonを教えてくださった先生、本連載で紹介した技術に関連する開発者の方々、執筆の場を提供してくださった代表、最後までお付き合いくださった読者のみなさまにお礼申し上げます。
-
- 2023年03月21日 『11 自然言語と人工言語のはざまで―ことばの研究・教育での言語処理技術の利用―:方言とテキスト生成:「ほんまに関西弁しゃべれんの?」 野口大斗(東京医科歯科大学ほか非常勤講師)』
-
方言とテキスト生成:「ほんまに関西弁しゃべれんの?」
テキスト生成
いよいよ連載も残り2回となりました。前回の記事で、大阪方言と話し言葉に関わることを扱うと予告していました。Hugging Face(1)を利用して、日本語のテキスト生成モデルを関西方言(関西弁)にファインチューニング(調整)してみます。計算機に方言を話させてみよう(テキストで出力させる)という試みです。ここでは大阪以外の話者のデータも使うので、より正確に関西と表現しておきます。関西方言のアクセントについては研究しているのですが、方言の全般的なことに関しては母語話者であるというだけで、何かを語れるわけではありません。前置きはなしで、さっそく本題に入りましょう。
まずは、事前に学習されたモデルで、テキストを生成してみましょう。今回はファインチューニングにGPUが必要であるため、忘れずにランタイムをGPUに変更しておいてください。無料アカウントではGPUの利用時間に制限があり、つづけて使用していると接続が切れてしまうので注意が必要です。ランタイムの変更が済んだら、以下のコードを実行してみましょう。「あの。」につづく文の候補が5つプリントされます。今回の目標は、この出力を関西方言に近づけることです。今回もコードを用意しています。
関西弁コーパス!pip install transformers
!pip install sentencepiece
#事前学習モデルで「あの。」につづく文を生成する。
from transformers import AutoTokenizer, AutoModelWithLMHead
tokenizer = AutoTokenizer.from_pretrained("rinna/japanese-gpt2-medium")
model = AutoModelWithLMHead.from_pretrained("rinna/japanese-gpt2-medium")
input = tokenizer.encode("あの。", return_tensors="pt")
output = model.generate(input, do_sample=True, max_length=100, num_return_sequences=5)
for result in tokenizer.batch_decode(output):
print(result)
目標を達成するには、関西方言のデータが必要です。テキストの準備をしましょう。今回は関西弁コーパス(https://sites.google.com/view/kvjcorpus/ホーム/日本語/データファイル)の形態素タグなしKSJデータ(2)を利用します。ダウンロードの際には、使用契約欄のライセンスについて確認してください。ダウンロードが完了したら、フォルダーを展開します。コーパスの選定理由は正書法に近い表記が使用されていて、関西方言のまとまったデータが公開されていることです。今回のタスクでは1回あたりの発話の量の多いほうが望ましいですが、弱点としてはインタビュー形式になっていて、短いやり取りが多いことです。対応方法については後述します。
すこしコーパスの話をしておくと、調べてみると方言のコーパスデータはかなり少ないことに気がつきます。書き言葉では話し言葉と違い、東京方言以外の話者でも東京方言を使用することが多いためです。Steve Jobsによるスタンフォード大学卒業式のスピーチの日本語版(関西方言)字幕(3)のようなものが簡単に見つかると思っていたのですが、そうではありませんでした。国立国語研究所の日本語諸方言コーパス(4)の利用も検討したのですが、方言の記述についてはカタカナ表記になっていました。また、文節単位での分かち書きになっていて、形態素解析が必要となります。カタカナで辞書に登録されていない語はそれ以上分割されないようになっている形態素解析器が多く、ひらがなにしてから解析しても精度はあまり高くありませんでした。漢字かな交じり文に変換することは困難な状態でした。
ファイルのアップロードとテキストの結合
さて、話を作業のほうに戻しましょう。以前と同様にGoogle Driveをマウントせずに、展開したファイルの中のテキストファイルをアップロードします。テキストファイルを132個すべてアップロードした場合には、ファインチューニングに45分ほどかかりました。許容できる待ち時間に合わせて、分量を調節してください。本格的に取り組みたい場合は、すべてのデータを利用するか、行ごとに無作為に並べ替えてデータを絞るのがよいでしょう。
まずは、アップロードしたファイルのテキストをすべて読み込みます(5)。KSJから始まって.txtで終わるファイルのテキストを結合しています。1000文字をプリントしてみていますが、インタビュアーと協力者を示す文字列と改行記号を確認することができます。
前処理#すべてのファイルを読み込み、テキストを結合する。
import os
text_train = ''
file_list = os.listdir('/content/')
for file in file_list:
if file.startswith('KSJ') and file.endswith('.txt'):
f = open(file)
text_train = text_train + f.read()
f.close()
text_train[:1000]
ある程度の長さの文を用意したいのですが、インタビューのため短い発話しかない部分もあります。また、今回のものは1行単位で話者記号がついているので、長い発話も話者記号で区切ると短くなってしまいます。そこで、今回は複数行にわたる協力者の発話のみを取り出して、改行記号でつなげています。具体的には、インタビュアーの発言ごとにリストにし、それをさらに改行記号ごとのリストにしています。リストの最初の要素は、インタビュアーのものであるため使用しません。最初にインタビュアーの記号ごとに分割しているので、この段階でインタビュアーのものを含めて改行(話者)記号が3つ以上あれば、インタビュアーの発言を除いて、協力者の発言が2行分以上残ることを意味します。つまり、複数行にわたって協力者が話していることになります。こちらも1000文字分プリントしておきます。
train.txtという名前をつけて、結合したテキストを書き込んだファイルを保存します。アップロードしたファイルと同じ階層に、train.txtというファイルが作成されているのに気づくはずです。こちらにアップロードしたデータは、ノートブックの接続が切れてしまうと消えてしまいます。必要があれば、ファイルにカーソルをかざしたときに出てくる3つの点のマークからダウンロードしておきましょう。#協力者が複数行にわたって発言している部分のみを抽出する。
text = text_train.split('*i: ')
extracted_text = ''
for line in text:
temp = line.strip().split('\n')
if len(temp) >= 3:
cont = ''.join(temp[1:])
cont = cont.replace('*s: ','')
cont = cont.replace(' ','')
extracted_text = extracted_text + cont + '\n'
extracted_text[:1000]
ファインチューニング#ファイルに保存する
f = open('train.txt', 'w')
f.write(extracted_text)
f.close()
GPUが使用されているかどうかを確認します。ファインチューニングの部分のコードは、CPUでは動作しません。
リポジトリ―のクローンとライブラリーのインストールをします。最初にtransformersを入れているのでエラーが出ますが、上から順にコードを実行するうえで動作に支障はありません。#GPUの使用を確認する。
!nvidia-smi
ファインチューニングするためのコードです。Hugging Faceから提供されているコードを走らせます。時間がかかるのでしばらく待ちます。!git clone https://github.com/huggingface/transformers -b v4.23.1
!pip install evaluate
コードの実行が終了すれば、モデルのトレーニングは終わりです。さっそくモデルを使って、テキストを生成してみましょう。ファイルの使用数などの実行環境によって、結果は変化します。「うまいこといってまっか?」(満足のいくものになったでしょうか。)「あの。」をほかの文字列に変えて試してみてください。使用したテキストのドメイン(領域)に偏りがあるので、うまくいかないこともよくあるはずです。#ファインチューニングする。
%%time
!python ./transformers/examples/pytorch/language-modeling/run_clm.py \
--model_name_or_path=rinna/japanese-gpt2-medium \
--train_file=train.txt \
--validation_file=train.txt \
--do_train \
--do_eval \
--num_train_epochs=3 \
--save_steps=5000 \
--save_total_limit=3 \
--per_device_train_batch_size=1 \
--per_device_eval_batch_size=1 \
--output_dir=output/
(方言の)データと言語研究の今後#ファインチューニングしたモデルを確かめる。
from transformers import AutoModelForCausalLM
model_rev = AutoModelForCausalLM.from_pretrained("output/")
input = tokenizer.encode("あの。", return_tensors="pt")
output = model_rev.generate(input, do_sample=True, max_length=100, num_return_sequences=5)
for result in tokenizer.batch_decode(output):
print(result)
本題からはそれますが、最後に方言のデータと言語の研究について言及しておきます。これまでの方言の研究では、記述や録音、分析が精一杯であり最善でした。ところが、言語処理技術の発展によって、大規模言語モデル(LLM)が生活様式を根底から覆そうとしている歴史の転換点の今、状況は変わりました。私たちは計算機上に言語モデルを残すという、かつてなかった選択肢を持っています。これを可能にするには、方言が消滅する前に大規模な機械可読かつオープンソースのデータを蓄積することが必要条件です。また、方言だけに話はとどまりません。日本語自体も今世紀末に母語話者の大部分を占める国内の人口が半減するという推計通りになれば(6)、数百年後には方言と同じ消滅の運命をたどるのかもしれません。言語学に求められているのは教育でも分析でも論文(7)を書くことでもなく、データを集めることなのかもしれません。
さて、連載もいよいよ来週で最終回を迎えることとなりました。執筆が決まったのは昨年の夏のことで、そのときにはもう少し初歩的なプログラミングの話をする予定でした。ChatGPTのようなモデルが世間をにぎわせることも、連載中にGPT-4が出てくることも想像していませんでした。しかし、そのころに時を戻すことはできません。私たちがこれからできることは言語モデルとうまく付き合い、選択的に言語処理を委託することでしょう。次週は技術をフル活用するうえで欠かせない、APIについての紹介で連載を締めくくります。それでは、最終回の更新をお楽しみに!
注
(1) https://huggingface.co
(2) ヘファナン・ケビン(2012)「関西弁コーパスの紹介」『総合政策研究』41号 157-164.
(3) https://www.youtube.com/watch?v=UF8uR6Z6KLc
(4) https://www2.ninjal.ac.jp/cojads
(5) osというライブラリーを使用しました。Pythonをローカル環境で使用する場合は、ファイルの処理によく使えます。
(6) https://www8.cao.go.jp/shoushi/shoushika/whitepaper/measures/w-2005/17webgaiyoh/html/hg110300.html
(7) そう遠くない未来に言語モデルに研究結果を報告したり、先行研究を問い合わせたりする時代がやってくるのではと期待しています。
-
- 2023年03月14日 『10 自然言語と人工言語のはざまで―ことばの研究・教育での言語処理技術の利用―:機械翻訳とライティング 野口大斗(東京医科歯科大学ほか非常勤講師)』
-
機械翻訳とライティング
言語処理技術の発展と言語学習
「機械翻訳は使い物になりますか?」2016年以前にこの質問をしたら、多くの人は使い物にならないと答えたでしょう。しかし、同年の11月にGoogleがニューラル機械翻訳を導入し、Google翻訳の性能は一夜にして変わりました(1)。それ以前は翻訳の性能以前に、出てくる訳文が文法的でさえないことがよくあったのです。現在、少なくとも言語資源が豊富な言語ペアでは、多くの学習者よりもはるかに自然な訳文を出力します。このころから言語教育を支えてきた実用性という1つの大きな価値観は揺らぎだしました。
さらに打撃がくわえられたのは、COVID-19の影響によるオンライン授業や、その後一般的になったBYOD(Bring Your Own Device)の対面授業でした。これらの出来事は、機械翻訳が実用的になる前からの惰性で続けられてきた、言語教育を見つめなおすきっかけになりました。言語能力はもはや電子機器の使用を制限した試験環境でしか測定できるものではないことが、まざまざと眼前に突きつけられたのです。ChatGPTなどの大規模言語モデルによりテキストが生成される現在では、人間が翻訳元のテキストを母語で産出したことさえ保証されなくなっています。
このような現状の中で外国語教育は必要なのでしょうか。近い将来(2)の実用的な観点だけで見ると答えは間違いなく否でしょう。日常的にその言語を必要としないかぎり、テクノロジーに頼ることで万事解決しそうです(3)。だからといって、言語学習が完全になくなるとも思えません。興味や関心があって続ける人々は一定数いるでしょうし、ビジネスなどである程度外国語でやり取りをする機会があれば、翻訳の内容が大きく意味を損ねていないか検証できる能力は必要でしょう(4)。
また、頻繁に外国語を使う必要があって、機械翻訳の出力をポストエディットしたい場合には、機械翻訳以上の言語能力が必要になってきます。今回はテクノロジーを使用せずに言語を使用できる能力を身につけたい場合に、どのように言語処理技術を学習の過程で活用できるのか、英語のライティングを例にとって考えてみます。
既存のツールを使ったライティング
最終的に機械に頼らないことが目標であるなら、まずは自力で英文を書いてみることが必要です。何も使わずに頭の中にある知識だけで英語を紡ぎだしてみてください。つぎにわからないことは辞書(5)をひいてみましょう。単語同士のつながりが適切かどうかを調べるには、コロケーション辞書を使ってみます。より洗練された語がないかを探したり、繰り返しを避けたりするにはシソーラス(類義語辞典)(6)を使います。同じ構造の文が繰り返されていないか、冗長な表現はないかも見直します。ここまでできたら、自分で文法の確認をしてみましょう。完成したら、いよいよテクノロジーの出番です。
はじめにスペルチェックをかけてみましょう。つづけて、オンライン上の英文校正サービスを利用します。GrammarlyやLanguageTool、DeepL Write(7)などの使用が想定されます。ChatGPTに「文法の間違いを指摘して」とお願いしてみるのもよいでしょう。ここまでできたら、英語を日本語にDeepLなどで機械翻訳してみましょう。言いたかったことが伝わっているでしょうか。もしおかしいようであれば、英語を書き換えてみましょう。そして、逆翻訳(出てきた日本語を再度英語に)し、出てきた英文を自分が書いたものと見比べます。機械翻訳のもののほうがよければ、そちらを採用しましょう。この段階で日本語を編集してみるのもよいでしょう。DeepLでは出力結果の語をクリックすると、ほかの候補を表示することもできます。
添削の自動化
さきほどまでは書く側の立場で見てきましたが、教える側はテクノロジーをどう使えるのでしょうか。基本的には人間の視点から確認してコメントすればよいわけですが、違うシステムあるいは複数のもので教員側でもチェックをかけたいかもしれません。その場合のために、ここでは登録不要かつ無料で使えるLanguageToolのAPI(8)を紹介しておきましょう。今回も入力済みのコードも用意してあります。機械学習の部分のコードは「CPU」で実行するとかなり時間がかかります。実行前に「ランタイム」-->「ランタイムのタイプを変更」から「GPU」を選択しておきましょう。
つぎのURLをブラウザに貼りつけてみてください。%20はスペースを表していて、This%20are%20the%20correct%20sentences.&language(This are the correct sentences.)をen-US(アメリカ英語)で校正せよという意味です。JASONという形式でデータが返ってきます。
Pythonから実行してみましょう。まずはURLを使ったパージョンです。https://api.languagetoolplus.com/v2/check?text=This%20are%20the%20correct%20sentences.&language=en-US
ここではライブラリーから使用してみましょう。#URLを使用した校正
import requests
import json
url = 'https://api.languagetool.org/v2/check'
text = 'This are the correct sentences.'
params = {
'text': text,
'language': 'en-US'
}
response = requests.post(url, data=params)
result = json.loads(response.text)
result
つぎのようにプリントすると見やすいかもしれません。#ライブラリーを使用した校正
!pip install language-tool-python
from language_tool_python import LanguageTool
text = " This are the correct sentences."
tool = LanguageTool('en-US')
matches = tool.check(text)
matches
ちなみに翻訳に関しては、別のライブラリーを使用します。for error in matches:
print(error)
2つを組み合わせると、エラーメッセージを翻訳することができます。import requests
def translate(text):
endpoint = "https://api.mymemory.translated.net/get"
params = {
"q": text,
"langpair": "en|ja"
}
response = requests.get(endpoint, params=params)
data = response.json()
return data
text = " This are the correct sentences.s"
translation = translate(text)
print(translation['responseData']['translatedText'])
LMS(Learning Management System)にテキストで提出するようにしておくとCSVファイルにダウンロードし、自動でエラーのコメントを入れて、一括アップロードすることができるようになります。ファイルで提出にすると不毛な手作業が増えるので、注意が必要です。for error in matches:
translation = translate(error.message)
print(translation['responseData']['translatedText'])
教材評価のための機械翻訳
また、教材作成の観点からも見てみましょう。一風変わっていますが、教材の評価にも使用できます。たとえば、何冊かの本を学習者が読んだ際に、明示的な説明なしに文ごとに意味だけ教えれば、文法や語彙を身につけることができるのかどうかを調べることにも活用できます。この方法であれば、人間を被験者にしないので、統制群を設けることによる倫理的な問題も発生しません。また、機械は学んだことを完璧に忘れてくれるので、同じモデルを使うと同じ条件で比較できます。どう機械翻訳モデルがトレーニングされるのかということを味わっていただくために、サンプルを用意しました。まず、データを読み込んでみましょう。
実際にプリントすると、以下のような文であることがわかります。日本語のモデルの訓練に使うデータのうち10文をプリントしてみましょう。!git clone https://github.com/odashi/small_parallel_enja.git
with open('/content/small_parallel_enja/train.en', 'r', encoding='utf-8') as f:
english_sentences_train = f.readlines()
with open('/content/small_parallel_enja/train.ja.000', 'r', encoding='utf-8') as f:
japanese_sentences_train = f.readlines()
with open('/content/small_parallel_enja/test.en.000', 'r', encoding='utf-8') as f:
english_sentences_test = f.readlines()
with open('/content/small_parallel_enja/test.ja', 'r', encoding='utf-8') as f:
japanese_sentences_test = f.readlines()
英語はつぎのようになります。japanese_sentences_test[:10]
データ数は500文です。もちろん実用的なものを作るには、数百万文以上のデータが必要だと言われています。english_sentences_test[:10]
検証結果は以下の通りです。まったく実用性がないことがわかると思います。翻訳システムの性能を上げるために、モデルの改善と、時間と計算機資源をかけた大量のデータでのトレーニングが不可欠なことが、身に染みて感じられます。便利な技術の裏には、泥臭い試行錯誤が隠れているのです。import tensorflow as tf
import numpy as np
# データを変数に与える
japanese_sentences = japanese_sentences_train
english_sentences = english_sentences_train
# 日本語文を整数に変換する
japanese_tokenizer = tf.keras.preprocessing.text.Tokenizer()
japanese_tokenizer.fit_on_texts(japanese_sentences)
# 英語文を整数に変換する
english_tokenizer = tf.keras.preprocessing.text.Tokenizer()
english_tokenizer.fit_on_texts(english_sentences)
# 整数を単語に変換するための逆引き辞書を作成する
japanese_index_word = {index: word for word, index in japanese_tokenizer.word_index.items()}
english_index_word = {index: word for word, index in english_tokenizer.word_index.items()}
# 日本語文を整数に変換する
japanese_sequences = japanese_tokenizer.texts_to_sequences(japanese_sentences)
# 英語文を整数に変換する
english_sequences = english_tokenizer.texts_to_sequences(english_sentences)
# パディング(0で埋めることによる文の長さの調整)する
japanese_padded = tf.keras.preprocessing.sequence.pad_sequences(japanese_sequences, padding='post')
english_padded = tf.keras.preprocessing.sequence.pad_sequences(english_sequences, padding='post')
# モデルを定義する
model = tf.keras.models.Sequential([
tf.keras.layers.Embedding(len(japanese_tokenizer.word_index)+1, 256),
tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(256)),
tf.keras.layers.RepeatVector(len(english_padded[0])),
tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(256, return_sequences=True)),
tf.keras.layers.Dense(len(english_tokenizer.word_index)+1, activation='softmax')
])
# モデルをコンパイルする
model.compile(loss='sparse_categorical_crossentropy', optimizer='adam')
# モデルを訓練する
model.fit(japanese_padded, english_padded, epochs=30)
さて、今回の例はコードを簡潔にして待ち時間も減らすために、モデルもさほど性能の出ない簡単なものを使い、トレーニングの回数もかなり少なくしています。教材の検証のためには、まず実際に大きなサイズのデータでなら十分に使い物になるモデルを作る必要があります。しかし、コーパスではうまく訓練されるモデルであっても、高校3年分の教科書の英文を学習させてみると、単語の対応関係さえも認識しないことがわかります。# 翻訳したい日本語の文をリストで変数に与える
input_sentences = japanese_sentences_test[:20]
# 日本語の文を整数に変換する
input_sequences = japanese_tokenizer.texts_to_sequences(input_sentences)
# パディングする
input_padded = tf.keras.preprocessing.sequence.pad_sequences(input_sequences, padding='post', maxlen=len(japanese_padded[0]))
# 翻訳する
output_sequences = model.predict(input_padded)
# 翻訳された英語の文を単語に変換する
output_sentences = []
for seq in output_sequences:
output_sequence = np.argmax(seq, axis=-1)
output_sentence = []
for i in output_sequence:
if i == 0:
break
if i in english_index_word:
output_sentence.append(english_index_word[i])
output_sentence = ' '.join(output_sentence)
output_sentences.append(output_sentence)
# 翻訳結果を表示する
for i, sentence in enumerate(input_sentences):
print("日本語{}:{}".format(i+1, sentence))
print("英語{}:{}".format(i+1, output_sentences[i]))
print()
高校の教科書でしか英語に触れない学習者は、英語を使っていく中で語彙や文法を自然に覚えてしまうのではなく、意識的に覚えている語を無理やりつなげて意味を解釈している可能性が高いです。これは教科書の紙幅と、一定数の新出単語を掲載するために同じ語を何度も使用できないことに起因します(9)。英語ができるようにならないのは、圧倒的に量が不足しているのです。
言語学ではかつて言語獲得は特別な能力であり、臨界期を過ぎると母語話者並みの言語能力を得られないとされていました。しかし、Transformer(10)というモデルが、言語処理のタスクにおいて高い性能を示すことが知られるようになりました。このモデルを応用したものが、画像認識などのほかの認知に関わるタスクでも、高い精度を叩き出しているのです。このことは母語話者レベルの言語運用が、一般的な認知能力で可能であることを示唆しています。言語処理技術は外国語を丸投げしてしまうのにも、教育にも、言語習得の調査にも活用できます。
さて、今週はここでおしまいです。来週は私の母語である大阪方言や話し言葉について扱う予定です。それでは次週の更新をお楽しみに!
注
(1) https://www.nytimes.com/2016/12/14/magazine/the-great-ai-awakening.html
(2) 近い将来と予防線を張ったのは、日本語母語話者の人口が著しく減少する見込みだからです。
(3) もちろん電力とインターネットの安定供給が前提です。
(4) 会計の際に暗算できなくても、1桁金額が違えば気がつく能力と同じくらいの位置づけといえばわかりやすいでしょうか。
(5) 機械翻訳を単語の検索に使うことは推奨しません。機械翻訳では、句以上の長めの単位で入力するのが望ましいです。
(6) 意外と知られていないようですが、Microsoft Wordにも付属しています。Googleで語の後ろにsynonymsとつけて検索するのも便利です。
(7) https://www.grammarly.com
https://languagetool.org
https://www.deepl.com/write
(8) Application Programming Interfaceの略で、人間とソフトウェアではなくソフトウェア同士で使われるインタフェースです。登録が必要なタイプのものは最終回で扱う予定です。
(9) 試しにカウントしたことのある、比較的よく使われている検定教科書では、3年間のリーディングの教科書で3000文を少し超える程度でした。実際にはすべて扱わない場合もあるでしょうし、リーディング以外のクラスもありますが、検定教科書だけでは1万文さえ超えないでしょう。本文の分量は以前より増加していますが、個人的には1授業に1セクションではなく1ユニット扱うぐらいでも妥当な気がしています。
(10) 今回のサンプルでは使用していませんが、Google翻訳の性能が劇的に変わるきっかけになりました。Attention is all you needという印象的なタイトルの論文で発表されました。
-
- 2023年03月07日 『9 自然言語と人工言語のはざまで―ことばの研究・教育での言語処理技術の利用―:Praatと音響分析 野口大斗(東京医科歯科大学ほか非常勤講師)』
-
Praatと音響分析
PraatとPython
前回は教育での利用を前提に、音声を文字化して収集するアプリを取り扱いました。しかし、言語学や言語教育の研究では、実際の音声そのものを収集して分析するほうが一般的です。今回は、Praatというソフトを利用して、収集した音声を分析する方法に触れます。とはいっても、Praatについての解説は日本語でも見つかりやすいので(1)、簡単に概観したあとで、Pythonを使用して分析を自動化する方法について紹介します。Pythonのコードに関しては、こちら入力済みのファイルも用意しています。
Praatとそのダウンロード
Praatはオランダのアムステルダム大学の研究者らによって開発された音響分析ソフト(2)です。フリーで公開されているので、さっそく使ってみましょう。まずはウェブサイト(https://www.fon.hum.uva.nl/praat)からダウンロードしましょう。"Download Praat"から使用しているOSを選択し、遷移先のページで条件に合ったものを選びます。ダウンロード後は解凍するだけですぐにプログラムを実行することができますが、フリーソフトのため初回実行時に警告メッセージの表示される可能性があります。
音声の録音とピッチの測定
さて、ダウンロードしたファイルを実行すると、2つのウィンドウが表示されます。Praat Pictureのほうは、可視化の結果を論文などに貼りつけられるように出力する際のお絵かきに使うものです。今回は使用しないので、閉じてしまいましょう。デモには音声ファイルが必要です。すでに録音されているものは"Open" --> "Read from file"から読み込めます(3)が、せっかくですので録音してみましょう。
モノラル(4)の音声を録音するには、"New"-->"Record mono Sound"-->"Record"の順にクリックして、音声を吹き込みます。任意の文を録音してみます。録音後に"Stop"-->"Save to list & Close"とクリックすると、最初に開いたウィンドウに"Sound untitled"というオブジェクトができているはずです。そのオブジェクトを選択し、"View & Edit"をクリックすると、音声波形とスペクトログラムが表示されます。下の該当する区間(たとえばTotal duration)のバーの部分をクリックすると、音声が再生されます。部分的に選択して再生することも可能です。再生にはTabキーを使用することもできます(5)。
それではピッチを測定してみましょう。ピッチが表示されていなければ、"Pitch"-->"Show pitch"とクリックします。ピッチが表示されている適当な範囲を選択して、"Pitch"-->"Get minimum pitch"を押すと、選択範囲内のピッチの最小値が表示されます。"Get maximum pitch"で最大値、"Get pitch"で平均値、"Pitch listing"で0.01秒(10msec)ごとのピッチのリストが出力されます。お気づきの方も多いと思いますが、このような範囲の指定の仕方では測定のたびに結果が異なってしまいます。そこで、範囲を区切ってから計測することが一般的におこなわれます。
アノテーション
実際に範囲を区切ってラベルづけ(アノテーション)をしてみましょう。最初のウィンドウで音声のオブジェクトを選択します。"Annotate"-->"To TextGrid (silences)"-->"OK"とクリックすると、"TextGrid untitled"というオブジェクトができます。音声のほうと合わせて選択して、"View & Edit"を押します。すると、無音区間に"silent'、そうでない部分に"sounding"と表示されたtier(層)ができます。通常は音ごとにIPAやArpabetなどのいわゆる発音記号を使いますが、ここでは単語ごとに区切ってラベルをつけてみましょう。左下のボタンを活用して見やすい大きさにしながら、区切りたいところをクリックして出てくる丸印かEnterキーを押してください。縦線が入るので、その間の文字を編集します。最初のウィンドウに戻り、音声を選択した状態で、"Save"-->"Save as WAV file"として保存しましょう。"TextGrid untitled"のほうも保存します。こちらは"Save as text file"になります。音声が増えた場合、それぞれの区間の測定値を転記する作業は気が遠くなります。Praatでスクリプトを書く方法もありますが、今回はPythonで処理する方法を説明します。
アノテーションの自動化
ちなみに、自動的にアノテーションをおこなうこともできます。英語の場合にはMontreal Forced Aligner(MFA)(https://montreal-forced-aligner.readthedocs.io/en/latest/index.html)や日本語の場合にはJulius(https://julius.osdn.jp/index.php?q=ouyoukit.html)などがあります。前者はPython、後者はPerlで書かれています。興味があれば試してみてください。
Parselmouth
ここではParselmouthと呼ばれるライブラリーを使用します。まずは、ライブラリーをインポートします。
ファイルですが、今回はDriveをマウントしない方法を利用します。この方法ではファイルがDrive上には保存されないので、Colaboratoryとの接続が切れた時点でファイルをアップロードしなおす必要があります。ドライブをマウントする際のアイコンの並びで最も左にある「セッションストレージにアップロード」をクリックして、さきほどの音声とテキストグリッドのファイルをアップロードします。試しに音声ファイルを再生してみます。filenameの後ろのパスは、必要があれば以前に説明した方法で書き換えてください。!pip install praat-parselmouth
import parselmouth
つぎに波形を描いてみます。下の4行はなくても表示できますが、軸を整えたりラベルを表示したりするために追加しています。from IPython.display import Audio
Audio(filename="/content/untitled.wav")
つづけて、スペクトログラムも表示してみます。Draw_spectrogram関数は公式ドキュメントの例のものを利用していますので、自分で書く必要はありません。ここまででPraatに表示されている初歩的なものをPythonからも確認することができました。import matplotlib.pyplot as plt
sound = parselmouth.Sound("/content/untitled.wav")
plt.plot(sound.xs(), sound.values.T)
plt.xlim([sound.xmin, sound.xmax])
plt.xlabel("Time (s)")
plt.ylabel("Amplitude")
plt.show()
ピッチをリストしてみるにはつぎのようにします。def draw_spectrogram(spectrogram, dynamic_range=70):
X, Y = spectrogram.x_grid(), spectrogram.y_grid()
sg_db = 10 * np.log10(spectrogram.values)
plt.pcolormesh(X, Y, sg_db, vmin=sg_db.max() - dynamic_range)
plt.ylim([spectrogram.ymin, spectrogram.ymax])
plt.xlabel("time [s]")
plt.ylabel("frequency [Hz]")
import numpy as np
spectrogram = sound.to_spectrogram()
draw_spectrogram(spectrogram)
plt.show()
平均はリストから処理して得てもよいですが、ここでは別の方法を紹介しておきましょう。平均をとる際にうっかりしていると、欠損値も含めた数で割ってしまい、実際よりも小さな値になることがあるかもしれません。pitch = sound.to_pitch()
pitch_values = pitch.selected_array['frequency']
pitch_list = pitch_values.tolist()
pitch_list
TextGridToolsparselmouth.praat.call(pitch, "Get mean", 0.0, 0.0, "Hertz")
テキストグリッドの処理にもparselmouthを使用することができますが、ここではより使いやすいTextGridToolsを利用します。ここでは、アノテーションをした層のラベルを1つずつ見て、その開始時刻と終了時刻を求めています。その時刻情報を用いてさきほどの方法でピッチを求め、ラベルとピッチをペアで書き出しています。
今回はPraatを使用した音声の処理とその自動化について説明しました。マイクを持って現地に出かけなくても音声を収集できるケースが増えてきています。集められるデータの数が増えているので、こうした技術を積極的に活用できると、効率的にデータを処理できます。また、データの処理速度が上がると、多くのデータを収集する計画を立てることもできます。音質に関しても静かな部屋で録音してもらえれば、フィールドワークのものよりもノイズの少ない音声を入手できることもあります。この連載では音声データの収集自体については扱いませんが、単純な読み上げであれば、録音ファイルをフォームで集めることも考えられます。また、こちらは現在共著で執筆中の新書に譲りますが、JavaScriptをうまく活用すると、立ち合いなしに複雑な実験をオンラインで完結させることも可能です。!pip install tgt
import tgt
textgrid = tgt.read_textgrid("/content/untitled.TextGrid")
intervals = textgrid.get_tier_by_name("silences")
for interval in intervals:
start_time = interval.start_time
end_time = interval.end_time
text = interval.text
pitch_value = parselmouth.praat.call(pitch, "Get mean", start_time, end_time, "Hertz")
print(text, pitch_value)
次回はライティング教育に言語処理の技術をどう生かすことができるのかについてみていきます。ニューラル翻訳、オンライン授業に自動テキスト生成とエッセイやレポートの公正さについて考えさせられることが多い昨今、避けては通れないトピックです。今月に入りChatGPTのAPI(6)も公開され、言語を取り巻く状況がますます変化していくと予測されます。連載も終盤にさしかかってきましたが、ホットな話題を提供できればと思います。それでは次週の更新をお楽しみに!
注
(1) 日本語で読める書籍としては、『音声学を学ぶ人のためのPraat入門』(ひつじ書房)があります。
(2) Praatは音響分析ソフトですが、実験用に音声を編集することも可能です。リスニング問題作成などの一般的な音声編集には、Audacity(https://www.audacityteam.org)などが便利でしょう。
(3) 実は数式から音を作ったり、テキストを読ませたりして音声ファイルを用意することも可能です。
(4) 一般的にはモノラルの音声を使用します。ステレオから変換することも可能です。
(5) 音量は最初のウィンドウの”Modify”-->”Scale peak”から調節できます。音量を上げる場合にはノイズも大きくなることに注意が必要です。
(6) APIとは何かについては最終回で紹介予定です。https://openai.com/blog/introducing-chatgpt-and-whisper-apis
-
- 2023年02月28日 『8 自然言語と人工言語のはざまで―ことばの研究・教育での言語処理技術の利用―:アプリの改良とメールの自動送信 野口大斗(東京医科歯科大学ほか非常勤講師)』
-
アプリの改良とメールの自動送信
実用化を目指して
前回はスピーキング練習用のウェブブラウザ上で動作し、結果をGoogle Spreadsheetに送信するアプリを作成しました。ブラックボックス状態でコードを貼りつけましたが、今回は説明をくわえつつ、問題点を改善していきます。手作り感がありながらも、実際の教育の現場で使えるレベルを目指して、提出確認のメールを自動送信するところまで紹介します。
HTMLファイル
前回のコードの解説
まずは、HTMLファイルのほうから確認していきましょう。前回のコードに解説のコメントをくわえたものを示します。
ボタンの設置<!--HTMLファイルであることを宣言する。-->
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<!--フォームを作成する。-->
<form id="myForm">
<label for="name">名前:</label>
<input type="text" name="name" id="name"><br><br>
<label for="email">メールアドレス:</label>
<input type="email" name="email" id="email"><br><br>
<label for="result">認識結果:</label>
<!--音声認識を利用せずに打ち込むことを避けたいので、readonlyにする。-->
<textarea name="result" id="result" readonly> </textarea>
<!--ボタンを設置し、クリックされたときにsubmitForm()を実行する。-->
<input type="button" value="送信" onclick="submitForm()">
</form>
<!--JavaScriptを組み込む。-->
<script>
//音声認識のためのAPIを利用する。
SpeechRecognition = webkitSpeechRecognition || SpeechRecognition;
const recognition = new SpeechRecognition();
recognition.onresult = (event) => {
var result = event.results[0][0].transcript;
//認識結果でHTMLを書き換える。
document.getElementById("result").textContent = result;
}
//認識を開始する。
recognition.start();
//提出ボタンが押された際の動作を定義する。
function submitForm() {
google.script.run.processForm(document.forms[0]);
document.getElementById("myForm").reset();
}
</script>
</body>
</html>
現状ではページを読み込んだ段階で自動的に音声認識が開始されて、一定時間音声を入力しないと認識しなくなってしまいます。任意のタイミングでスタートとストップができるように、ボタンを設置しましょう。ボタンが押されたときに開始したいので、無条件に認識を開始するコードは削除します。
//認識を開始する。
recognition.start();
<!--ボタンを設置し、クリックされたときにsubmitForm()を実行する。-->
<input type="button" value="送信" onclick="submitForm()">
-->
<!--ボタンを設置し、クリックされたときに関数の動作を実行する。-->
<input type="button" value="スタート" onclick="startRecognition()">
<input type="button" value="ストップ" onclick="stopRecognition()">
<input type="button" value="送信" onclick="submitForm()">
もちろん、ボタンが押されたときに実行される関数もそれぞれ定義します。ここまでの変更で、自由なタイミングで開始と終了が可能になりました。
//提出ボタンが押された際の動作を定義する。function submitForm() {
google.script.run.processForm(document.forms[0]);s
document.getElementById("myForm").reset();
}
-->
//ボタンが押された際の動作を定義する。
連続認識と途中結果function startRecognition() {
recognition.start();
}
function stopRecognition() {
recognition.stop();
}
function submitForm() {
google.script.run.processForm(document.forms[0]);
document.getElementById("myForm").reset();
}
このままでは、1つ目の語しか表示されませんので、連続認識と途中結果の表示を有効にします。そのうえで、最初から最後までの語を結合していき、その結果でフォームの値を書き換えます。recognition.onresult = (event) => {
var result = event.results[0][0].transcript;
//認識結果でHTMLを書き換える。
document.getElementById("result").textContent = result;
}
-->
テキストボックスlet finalTranscript = '';
let interimTranscript = '';
recognition.onresult = (event) => {
interimTranscript = '';
for (let i = event.resultIndex; i < event.results.length; i++) {
if (event.results[i].isFinal) {
finalTranscript += event.results[i][0].transcript;
} else {
interimTranscript += event.results[i][0].transcript;
}
}
//認識結果でHTMLを書き換える。
document.getElementById('result').innerHTML = finalTranscript;
};
テキストボックスのサイズを調整しましょう。さらに、うまく認識されなかった箇所を意図したものにタイプしなおして修正できるように、もう1つテキストボックスを用意します。こうすることで、実際に話した内容を再度文字で確認する機会を提供することができます。利用者に貼りつけなおす手間をかけさせないために、認識結果も入れられるようにします。こちらは編集可能な状態にしておきましょう。<label for="result">認識結果:</label>
<!--音声認識を利用せずに打ち込むことを避けたいので、readonlyにする。-->
<textarea name="result" id="result" readonly> </textarea>
-->
リアルタイムでの表示<label for="result">認識結果:</label>
<textarea rows="10" cols="50" name="result" id="result" readonly> </textarea><br><br>
<label for="result_edit">編集結果:</label>
<textarea rows="10" cols="50" name="result_edit" id="result_edit"> </textarea>
認識結果をフォーム外にも表示できるようにします。認識後すぐの文字はグレーで表示するようにします。認識が終わったあとにまとめて表示するのではなく、順次結果を表示していくことで、利用者の発音が使用している音声認識モデルにとって聞き取り可能なものであるかどうかが、リアルタイムでフィードバックされます。<body>
<!--フォームを作成する。-->
<form id="myForm">
-->
<body>
<div id="result-div"></div><br>
<form id="myForm">
書き換える内容を指定してやります。document.getElementById('result').innerHTML = finalTranscript;
-->
他言語への対応document.getElementById("result-div").innerHTML = finalTranscript + '<span style="color: gray">' + interimTranscript + '</span>';
document.getElementById("result").innerHTML = finalTranscript;
document.getElementById("result_edit").innerHTML = finalTranscript;
これでフォームは完成です。すでに気づいた方もいると思いますが、デフォルトでは英語を認識するようになっていて、日本語には反応しません。例はこのまま英語で進めますが、日本語教育などで利用する場合は、明示的に言語を指定します。もちろん、ほかにも多くの言語に対応していますので、必要に応じて調べてみてください。const recognition = new SpeechRecognition();
-->
Google Apps Scriptconst recognition = new SpeechRecognition();
recognition.interimResults = true;
recognition.continuous = true;
recognition.lang = 'ja';
前回のコードの解説
前回のコード.gsではHTMLを表示し、フォームの要素をシートに書き込みました。
収集項目の追加//HTMLを表示する。
function doGet() {
return HtmlService.createHtmlOutputFromFile('index');
}
//バインドされているシートにフォームの情報を書き込む。
function processForm(form) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
sheet.appendRow([form.name, form.email, form.result]);
}
今回は認識内容の修正欄を作ったので、項目を追加します。sheet.appendRow([form.name, form.email, form.result]);
-->
このままでは随時、結果がシートに送信されてしまうので、送信ボタンが押された時に1度だけ送信するようにします。左側のタイマーマークにカーソルをかざして「トリガー」、つぎに右下の「トリガーの追加」をクリックします。「実行する関数を選択」には"processForm"を、「イベントの種類を選択」には「フォームを送信時」を設定して保存します。sheet.appendRow([form.name, form.email, form.result, form.result_edit]);
メールの自動送信
メールの自動送信(1)は以下のコードで実行可能です。フォームからの情報と時刻を変数に格納します。MailApp.sendEmail(2)の引数として、順にメールアドレス、件名、本文を与えれば、受領確認のメールを送信することができます。これをprocessForm内にくわえてみましょう。
利便性を考えてvar emailAddress = form.email;
var voice = form.result;
var text = form.result_edit;
var date = new Date();
MailApp.sendEmail(emailAddress, "課題提出確認",'課題を受け取りました。'+'\n'+'音声認識結果:'+'\n'+voice+'\n'+'修正結果:'+'\n'+text+'\n'+date);
同じ組織内のアカウントを持っている場合には、メールアドレスを自動収集することが可能です。以下のように書き換えると、自動収集されたアドレスにメールが送信されるため、名前とメールアドレスの入力が不要になります。var emailAddress = form.email;
-->
var emailAddress = Session.getActiveUser().getEmail();
Googleのアカウントにログインした状態で、リンクをクリックするだけで使用できるようになります。そのため、LMSなどよりも操作のストレスは少なくて済みます。また、名前やメールアドレスの打ち間違いもなくなります。メールアドレスと学籍番号に対応関係のある場合には、受け取る側の処理も簡単です。法則がない場合でも、Googleフォームなどで一度メールアドレスを把握すれば問題ありません。名前やメールアドレスの打ち間違いもなくなります。それだけではなく、なりすましもアカウントが適切に管理されている限りにおいて起こりにくいです。
最後に書き換え後のHTMLとGoogle Apps Scriptのコードを掲載しておきます。前回の手順にしたがってデプロイすることで使用できるようになります。<!--index.html-->
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<body>
<div id="result-div"></div><br>
<form id="myForm">
<form id="myForm">
<label for="name">名前:</label>
<input type="text" name="name" id="name"><br><br>
<label for="email">メールアドレス:</label>
<input type="email" name="email" id="email"><br><br>
<label for="result">認識結果:</label>
<textarea rows="10" cols="50" name="result" id="result" readonly> </textarea><br><br>
<label for="result_edit">編集結果:</label>
<textarea rows="10" cols="50" name="result_edit" id="result_edit"> </textarea>
<input type="button" value="スタート" onclick="startRecognition()">
<input type="button" value="ストップ" onclick="stopRecognition()">
<input type="button" value="送信" onclick="submitForm()">
</form>
<script>
SpeechRecognition = webkitSpeechRecognition || SpeechRecognition;
const recognition = new SpeechRecognition();
recognition.interimResults = true;
recognition.continuous = true;
recognition.lang = 'en-US';
let finalTranscript = '';
let interimTranscript = '';
recognition.onresult = (event) => {
interimTranscript = '';
for (let i = event.resultIndex; i < event.results.length; i++) {
if (event.results[i].isFinal) {
finalTranscript += event.results[i][0].transcript;
} else {
interimTranscript += event.results[i][0].transcript;
}
}
document.getElementById("result-div").innerHTML = finalTranscript + '<span style="color: gray">' + interimTranscript + '</span>';
document.getElementById("result").innerHTML = finalTranscript;
document.getElementById("result_edit").innerHTML = finalTranscript;
};
function startRecognition() {
recognition.start();
}
function stopRecognition() {
recognition.stop();
}
function submitForm() {
google.script.run.processForm(document.forms[0]);
document.getElementById("myForm").reset();
}
</script>
</body>
</html>
今回は実践的なウェブアプリの作成について学びました。アプリを作る機会は多くないかもしれませんが、経験してみることで適切な情報の収集方法について意識するきっかけになるのではないでしょうか。相手に負担がかからず加工しやすいデータを集められることは重要です。言語処理技術を活用する機会が多くない方も、無駄な事務作業や独自書式のワードファイルやエクセルファイルでの情報のやり取りをなくすヒントが、今週の記事にあったのではないでしょうか。言語とは関係がありませんが、メールの送信も覚えておくと汎用性があります。//コード.gs
function doGet() {
return HtmlService.createHtmlOutputFromFile('index');
}
function processForm(form) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
sheet.appendRow([form.name, form.email, form.result, form.result_edit]);
var emailAddress = form.email;
var voice = form.result;
var text = form.result_edit;
var date = new Date();
MailApp.sendEmail(emailAddress, "課題提出確認",'課題を受け取りました。'+'\n'+'音声認識結果:'+'\n'+voice+'\n'+'修正結果:'+'\n'+text+'\n'+date);
}
さて、次週は文字化された音声ではなく、実際の音声について見ていきます。来週の更新をお楽しみに!
注
(1) 自信がない場合はテスト用のアカウントの使用をお勧めします。
(2) 送信専用の関数のため、メールを削除してしまう心配がありません。
-
- 2023年02月21日 『7 自然言語と人工言語のはざまで―ことばの研究・教育での言語処理技術の利用―:音声認識とスピーキング 野口大斗(東京医科歯科大学ほか非常勤講師)』
-
音声認識とスピーキング
認識結果の収集
前回に引き続き、今回も実用的な応用の可能性のある技術について紹介します。今回と次回の記事では、オンラインでの音声収集についてみていきます。しばらくPythonから離れます。今回の第1弾は、音声を認識してその認識結果をスプレッドシートに表示するウェブアプリを作ることがテーマです。たとえば、今回の方法を使用することで、スピーキングの課題(1)を作成することができます。音声自体は収集しないため、プライバシーなどの問題もなく、文字化されているので、フィードバックも比較的簡単におこなえます。
かなりコードが複雑になってきますが、ブロック単位でざっくりと何をしているのかがわかれば、問題ありません。コピーしてうまく動作すれば大成功です。積み上げ式だけではなく、実際にアウトプットする形式から細分化して、必要なことを調べていくという選択肢を持っておくことは有用です。自身の経験を振り返ってみると、何かをよく学んだのは実際にそれが必要になったときでした。Pythonに関しては、大学院のウェブサイトの更新担当でHTMLファイルを一括して書き換えるスクリプトの読み方を学んだとき、研究員の仕事や研究プロジェクトで大量のファイルを処理しなければならなかったとき、言語に関するプログラムの処理を業務委託されたときでした。英語に関しては、英文学科で学んでいたときでも、交換留学していたときでもなく、はじめて毎週90分間英語だけで授業をしなければならなくなったときでした。JavaScriptに関しては、日本語で運営している英語のリスニングを含む授業(2)で、学生のスピーキングのアウトプットの機会を設けたいと思い立ったとき、いまこの連載を書いている瞬間です。
いまは何をしているのかわからなかったとしても、必要に迫られて読み返してみたときに、それぞれのコードの意味がわかるかもしれません。また、実際にアウトプットが必要になったときに、それまでインプットしてきたこととアウトプットに必要なことには乖離(3)があることに気づくかもしれません。覚えようとしたり、最初から完璧を目指したりせずに、必要なことはその都度調べて、わからないことはそっと頭の片隅にしまっておくくらいの気分で、気楽に取り組んでみてください。
JavaScript
まずは、JavaScriptを使用して音声を認識してみましょう。ウェブサイトはHTMLで書かれているという話を、第2回目のスクレイピングの紹介でしました。ほかにもCSSで見た目に関する設定がなされます。さらにJavaScriptはウェブサイトを動的に変化させるために使用されます。クリックしたりマウスをかざしたりしてページが変化するのは、JavaScriptのおかげなのです。第2回目の注にJavaScriptの停止方法が記載されているので、試してみてください。Amazonのような通販サイトやYouTubeのような動画サイトが実験に適していて、面白いでしょう。Amazonでは正常に動作するのはリンクのみだけで、写真が切り替えられなかったり、YouTubeに関しては、もはや動画投稿サイトではなくなってしまったりすることがわかります。JavaScriptの身近さとありがたさに身をもって気づくことでしょう。
さて、さっそく動かしてみましょう。今回はGoogle Chromeを使用している前提で話を進めます。ほかのブラウザを使用している場合は、適宜ウェブなどで検索して調べてみてください。コンソールという画面を使用します。Google Chromeの右上のボタンから、「その他のツール」-->「ディベロッパーツール」とクリックし、開いたウィンドウの"Console"をクリックします。つぎのように入力してEnterキーをクリックすると、文字列がプリントされます。
内蔵あるいは外付けのマイクを用意したうえで、以下のコードをおまじないだと思って、貼り付けてみてください。Web Speech APIというインターフェースを使用して、音声認識結果をプリントするようになっています。マイクの使用を許可して、マイクが反応しているあいだになにか英語で(4)話しかけてみてください。マイクの反応に間に合わなかった場合は、ページを再読み込みしましょう。認識された音声が文字としてプリントされます。console.log("Hello.")
このままでは開発者モードを開いたままにしておかないと認識文字列がわからないので、ポップアップして表示させてみます。console.logではなく、alertに変更して同じことを繰り返してみてください。マイクの使用許可を尋ねられたのと同じ形式で、音声認識結果が表示されるはずです。SpeechRecognition = webkitSpeechRecognition || SpeechRecognition;
const recognition = new SpeechRecognition();
recognition.onresult = (event) => {
console.log(event.results[0][0].transcript);
}
recognition.start();
HTMLへの組み込み
それでは、コードをHTMLに組み込んでみましょう。さきほどのコードをタグで囲ったものをテキストエディタに貼りつけて、拡張子をhtmlにして保存してみましょう。ブラウザから開くとこれまでと同様に動作するはずです。
実際に認識結果でページの内容を書き換えてみましょう。「音声認識結果」というプレイスホルダー(仮の文字列)を入れておき、認識結果で書き換えることができます。<script>
SpeechRecognition = webkitSpeechRecognition || SpeechRecognition;
const recognition = new SpeechRecognition();
recognition.onresult = (event) => {
alert(event.results[0][0].transcript);
}
recognition.start();
</script>
Google Apps Script<script>
SpeechRecognition = webkitSpeechRecognition || SpeechRecognition;
const recognition = new SpeechRecognition();
recognition.onresult = (event) => {
var result = document.getElementById("speech");
result.textContent = event.results[0][0].transcript;
}
recognition.start();
</script>
<p id="speech">音声認識結果</p>
このままでは、自分のコンピューター上でしか使用できないため、ウェブ上に公開してみましょう。通常はサーバーが必要ですが、ここではGoogleが用意しているものを利用します。Google Apps Scriptを使ってみましょう。Googleのアプリを操作することができる言語です。Drive上からでも以下のリンクからでも開けます。
https://script.google.com/home/projects/create
ファイルの横の「+」のマークをクリックし、HTMLを選択します。そこに、さきほどのコードを貼りつけて書き換えます。ファイル名は「無題」からindexにしておきましょう。すると、自動的に拡張子が付与されて、「index.html」となります。
「コード.gs」をクリックし、もとから表示されているコードを削除し、以下のものを貼りつけます。これで準備は完了です。<script>
SpeechRecognition = webkitSpeechRecognition || SpeechRecognition;
const recognition = new SpeechRecognition();
recognition.onresult = (event) => {
var result = document.getElementById("speech");
result.textContent = event.results[0][0].transcript;
}
recognition.start();
</script>
<p id="speech">音声認識結果</p>
function doGet() {
return HtmlService.createHtmlOutputFromFile('index');
}
フロッピーディスクマーク-->「デプロイ」-->「新しいデプロイ」の順にクリックします。「種類の選択」で「ウェブアプリ」を選び、「アクセスできるユーザー」を必要に応じて変更(5)してください。「デプロイ」をクリックして、ウェブアプリの下に表示されているURLからアクセスできるようになります。
コンテナバインド方式
さきほどのコードはスタンドアロンという独立タイプでしたが、アプリと結びついたコンテナバインドという方式を使用してみましょう。今回はスプレッドシートを使いたいので、まずアプリを開きます。Driveからでも以下のリンクからでも開くことができます。
https://docs.google.com/spreadsheets/u/0/create
「拡張機能」、"Apps Script"の順にクリックすると、さきほどと同じ画面が出てきます。しかし、今回のスクリプトは開いているスプレッドシートに紐づけられています。それでは、HTMLに以下のものをコピーしてみましょう。名前は今回もindexとしておいてください。中身はわからなくて構いません。興味があれば、HTMLのフォームの書き方とJavaScriptのイベントについて調べてみるとよいでしょう。
コード.gsのほうにはつぎのものを貼りつけておきましょう。ここも魔法の呪文との認識で構いません。HTMLを出力し、フォームに入力された名前とメールアドレスが、アクティブなシートに書き込まれます。<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<form id="myForm">
<label for="name">名前:</label>
<input type="text" name="name" id="name"><br><br>
<label for="email">メールアドレス:</label>
<input type="email" name="email" id="email"><br><br>
<input type="button" value="送信" onclick="submitForm()">
</form>
<script>
function submitForm() {
google.script.run.processForm(document.forms[0]);
document.getElementById("myForm").reset();
}
</script>
</body>
</html>
スピーキング課題アプリfunction doGet() {
return HtmlService.createHtmlOutputFromFile('index');
}
function processForm(form) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
sheet.appendRow([form.name, form.email]);
}
何が起こるかさきにお伝えしてしまいましたが、実行してみましょう。さきほどと同じ手順でデプロイしてみてください。警告メッセージを確認のうえ、許可を与えないと進めません。フォームに適当な文字列を入れて送信してみましょう。手元のスプレッドシートにデータが収集されたはずです。もう1度、違う内容でフォームを送信してみましょう。つづきの行にデータが追加されます。テキストだけであればGoogle Forms(6)でも簡単にできるので、今回の目的である音声認識と組み合わせてみましょう。HTMLは以下のようになります。
コード.gsはつぎのようにします。<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<form id="myForm">
<label for="name">名前:</label>
<input type="text" name="name" id="name"><br><br>
<label for="email">メールアドレス:</label>
<input type="email" name="email" id="email"><br><br>
<label for="result">認識結果:</label>
<textarea name="result" id="result" readonly> </textarea>
<input type="button" value="送信" onclick="submitForm()">
</form>
<script>
SpeechRecognition = webkitSpeechRecognition || SpeechRecognition;
const recognition = new SpeechRecognition();
recognition.onresult = (event) => {
var result = event.results[0][0].transcript;
document.getElementById("result").textContent = result;
}
recognition.start();
function submitForm() {
google.script.run.processForm(document.forms[0]);
document.getElementById("myForm").reset();
}
</script>
</body>
</html>
さて正常に動作したら大成功です。ライティングではなくスピーキングの練習をしてほしいので、認識内容は書き換えられない(7)ようにしています。長くなってきたので、今回はここまでにしましょう。次回は、コードの詳しい説明を加えながら、アプリを実用的に使えるようにしていきます。function doGet() {
return HtmlService.createHtmlOutputFromFile('index');
}
function processForm(form) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
sheet.appendRow([form.name, form.email, form.result]);
}
注
(1) ライティングの課題は一般的ですが、スピーキングの課題は録音データをアップロードさせる必要があり、通常速度で再生した場合には発話時間と同じだけ確認時間がかかるので、用いられることは稀です。また、ライティングでもそうですが、人数分だけファイルを処理しないといけない状況を作るのは間違いなく悪手です。
(2) 連載で紹介するアプリは、東京大学教養学部前期課程2022年度Aセメスターの「英語中級」のために開発したものを簡易化しました。当初、複数回確認メールが送信される不具合のある中、アプリを使用してくださった履修者のみなさんに感謝します。
(3) 英語の例を挙げると、文法の授業では選択式の試験を作りやすい文法項目を偏重することが多いです。しかし、実際に英語を書いたり話したりしてみると必要なのは、主語の選択、冠詞の使用、名詞の可算性、時制と動詞の屈折変化などであることが多いです。
(4) デフォルトの言語は英語になっています。指定することで他言語も使用可能です。
(5) 学校などの組織で発行されているGoogleアカウントでは、組織内のユーザーに限定することもできます。また、同一ドメインの場合はメールアドレスを収集できるため、メールアドレスと名前やIDなどの辞書を作成しておくと、それらの情報を入力する必要や打ち間違いによる修正などが不要になり、簡単に確認メールも送信できます。
(6) https://docs.google.com/forms
(7) 認識内容のフォーム欄がキーボード入力可能であると、音声入力を使用せずにキーボードで入力できてしまうため、制限をかけています。知識があれば制限をかいくぐることは可能です。
-
- 2023年02月14日 『6. 自然言語と人工言語のはざまで―ことばの研究・教育での言語処理技術の利用―:機械学習と著者推定 野口大斗(東京医科歯科大学ほか非常勤講師)』
-
機械学習と著者推定
日本語のテキスト
さて、連載も前半戦を終えようとしています。今回は、テキストの分類に挑戦します。テキストの分類にも、数多くの種類があります。たとえば、迷惑メールかどうか、レビューコメントの評価が高いか低いか、テキストのトピックが何であるかなどの分類です。その中でも、今回はテキストを著者によって分類してみましょう。具体的には、青空文庫のテキストを使用して、著者が芥川龍之介であるか、夏目漱石であるかを判定させてみます。技術的には、日本語の取り扱いに必要な形態素解析と、ナイーブベイズを使用した機械学習の方法を紹介します。今回も、コードを貼りつけたノートブックを用意しています。
青空文庫のテキスト処理
まず、青空文庫(1)のテキストを入手しなくてはなりません。著者の独断と偏見で、芥川龍之介と夏目漱石の有名なテキストを選びました。辞書形式にしてあるので、以下のコードをColaboratoryに貼りつけてください。作品を確認したい場合は、URLにブラウザからアクセスできます。それぞれの作家から6つずつ(2)作品(3)をとってきています。のちの処理の都合上、テキストの並び順を連番にしていません。
#青空文庫のテキストのURLを辞書形式で定義する。
urls = {'natsume1':'https://www.aozora.gr.jp/cards/000148/files/773_14560.html',
'natsume2':'https://www.aozora.gr.jp/cards/000148/files/776_14941.html',
'natsume3':'https://www.aozora.gr.jp/cards/000148/files/794_14946.html',
'natsume4':'https://www.aozora.gr.jp/cards/000148/files/752_14964.html',
'akutagawa1':'https://www.aozora.gr.jp/cards/000879/files/127_15260.html',
'akutagawa2':'https://www.aozora.gr.jp/cards/000879/files/179_15255.html',
'akutagawa3':'https://www.aozora.gr.jp/cards/000879/files/42_15228.html',
'akutagawa4':'https://www.aozora.gr.jp/cards/000879/files/43015_17432.html',
'natsume5':'https://www.aozora.gr.jp/cards/000148/files/789_14547.html',
'natsume6':'https://www.aozora.gr.jp/cards/000148/files/56143_50921.html',
'akutagawa5':'https://www.aozora.gr.jp/cards/000879/files/92_14545.html',
'akutagawa6': 'https://www.aozora.gr.jp/cards/000879/files/43016_16836.html'}
以前学んだスクレイピングを利用して、テキストを取り出すための関数を書いてみましょう。青空文庫は文字コードがshift_jisのため、エンコーディングを指定せずに読み込むと、文字化けが起こります。ふりがなのHTMLタグと、それにはさまれた部分は削除しています。本文のみを抽出し、スペースを処理するために、1度分割して再結合しています。ブラックボックスとして使っても構いませんが、興味があれば、それぞれの行で何をしているのか調べてみてください。
#引数に指定した青空文庫のurlからプレインテキストを取得する。
def aozora(url):
import requests
from bs4 import BeautifulSoup
webpage = requests.get(url)
#青空文庫はshift_jisのためエンコーディングを指定する。
webpage.encoding = "shift_jis"
soup = BeautifulSoup(webpage.content, 'html.parser')
#ふりがなを削除する。
for tag in soup.findAll(["rt", "rp"]):
tag.decompose()
#本文の最初の1ooo文字を抽出する。
text = soup.find(class_="main_text").get_text().strip()[:1000]
#スペースを処理する。
text = ''.join(text.split())
return text
関数の動きを確かめてみましょう。URLを引数として、スペースやふりがなを削除したテキスト1000文字が返ってくるようになっています。ここでは、先ほど定義したurlsのnatsume1に対応する『こころ』(夏目漱石)のURLが代入され、最初から1000文字が返ってきます。
#natsume1の値のurlを引数として関数を実行する。
natsume_test = aozora(urls['natsume1'])
natsume_test
形態素解析
英語と日本語を扱う際の大きな違いは、語の分割方法です。英語は語がスペースで区切られますが、日本語ではそうではありません。語を分割して英語のように扱うには、品詞分解(形態素解析)をする必要があります。今回は、pipコマンド(4)のインストールだけで簡単に使用できる、janomeというライブラリーを採用します。『こころ』の最初の100文字を、形態素解析してみましょう。
#janomeをインストールする。
!pip install janome
#「こころ」の最初の100文字を形態素解析する。
from janome.tokenizer import Tokenizer
for token in Tokenizer().tokenize(natsume_test[:100]):
print(token)
それぞれの要素を別に取り出すこともできます。品詞の並びを調べたり、特定の品詞だけを抽出したりすることも容易です。
#品詞、原形、表層形、読みをプリントする。
for token in Tokenizer().tokenize(natsume_test[:100]):
print(token.part_of_speech.split(',')[0], token.base_form, token.surface, token.reading)
分かち書き(単語分割)
ここで得られた表層形を単に連結してもよいのですが、ループなしで分かち書きが簡単にできるようになっているので、そちらを利用します。
#分かち書きをする。
' '.join(list(Tokenizer(wakati=True).tokenize(natsume_test[:100])))
それでは、著者とURLの辞書の項目数分だけループを回して、スクレイピングの関数を実行します。著者と実行結果を分かち書きしたものがペアになった辞書を作成します。
#スクレイピング結果を分かち書きして辞書にする。
texts = {}
for author, url in urls.items():
text = aozora(url)
wakati = Tokenizer(wakati=True)
text = list(wakati.tokenize(text))
text = ' '.join(text)
texts[author]=text
著者名と書き出しの50文字をプリントしてみましょう。
#著者名と書き出しの50文字をプリントする。
for author, text in texts.items():
print(author,text[:50])
Bag-of-words modelとTF-IDF
今回はそれぞれの単語が出現する回数(Bag-of-words model)を特徴量として利用します。特定の品詞を省く処理はしません。数学的な計算に使用するnumpyと機械学習に使用するscikit-learnというライブラリーをインポートします。内容が盛りだくさんになるため、ライブラリーの詳細(5)は割愛します。単語の出現回数をカウントし、その結果をnumpyの配列(ベクトルや行列)に変換しています。さらに、以前使用したpandasで表を表示しています。
#それぞれの作品における単語の出現回数をカウントする
import numpy as np
import pandas as pd
from sklearn.feature_extraction.text import CountVectorizer
text_array = np.array(list(texts.values()))
count = CountVectorizer()
count.fit(text_array)
x = count.transform(text_array)
x = x.toarray()
pd.DataFrame(x, columns=count.get_feature_names_out())
しかし、このままではすべての単語が同じだけの重みを持ってしまうため、TF-IDF (Term Frequency-Inverse Document Frequency)を使用します。TF-IDFとは、文書の総数をそれぞれの単語が出てくる文書の数の数で割って対数をとったものと、語の出現回数をかけ合わせた値です。値は語が出てきにくいものであるほど大きくなります。
#TF-IDFを求める。
import pandas as pd
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
text_array = np.array(list(texts.values()))
tfidf = TfidfVectorizer()
x = tfidf.fit_transform(text_array)
x = x.toarray()
pd.DataFrame(x, columns=count.get_feature_names_out())
ナイーブベイズ(機械学習)と著者推定
今回は、natsume1-natsume4とakutagawa1-akutagawa4のそれぞれの語のTF-IDFを入力として、夏目漱石(0)か芥川龍之介(1)を出力するモデルを訓練します。機械学習では、学習時に見せるデータとテスト用にとっておくデータに分割することが一般的です。分割は出力(正解)割合が等しくなるように(6)ランダムにおこなうのが理想的です。ナイーブベイズはパラメーターを変更していくタイプの機械学習ではないので、割合が等しくなくても問題ありません。今回はランダマイズしないので、正解のラベルはnatsume1から順に0,0,0,0,1,1,1,1,0,0,1,1となります。ランダマイズする場合は、特徴量と正解ラベルをペアにしておく必要があります。
#正解ラベルを付与しておく。
y = [0,0,0,0,1,1,1,1,0,0,1,1]
y = np.array(y)
y
natsume1-natsume4とakutagawa1-akutagawa4を訓練(train)データ(xとyそれぞれの0から8番目の前まで)、残りをテスト(test)データ(xとyそれぞれの8番目以降)とします。
#データを学習用とテスト用に分割する。
x_train, x_test, y_train, y_test = x[:8], x[8:], y[:8], y[8:]
以下のように書いてもおなじです。
#データを学習用とテスト用に分割する。
x_train = x[:8]
x_test = x[8:]
y_train = y[:8]
y_test = y[8:]
ナイーブベイズモデルを使って分類器(classifier)のモデルを作成し、テストデータでの正解率を計算するスクリプトです。ナイーブベイズでは、あるクラスに属する確率とそのクラスで特定の単語が含まれる確率のかけ算(7)をしています。
#ナイーブベイズモデル分類器の訓練と評価をする。
from sklearn.naive_bayes import MultinomialNB
clf = MultinomialNB().fit(x_train, y_train)
print("Accuracy:", clf.score(x_test, y_test))
くわしく結果を見ると、0, 0, 1, 1と予想すべきところで0, 1, 1, 1としたために、正解率が3/4(75%)となったことがわかります。
#正解ラベルと予想した答えをプリントする。
y_pred = clf.predict(x_test)
print(y_test, y_pred)
また、つぎのような混合行列で可視化することも可能です。分類するクラスが3つ以上になった際に特に有用です。
#混合行列を用いて可視化する。
from sklearn.metrics import confusion_matrix
import matplotlib.pyplot as plt
import seaborn as sns
cm = confusion_matrix(y_test, y_pred)
sns.heatmap(cm, annot=True, cmap='Blues')
plt.xlabel('Predicted')
plt.ylabel('True')
いかがだったでしょうか。すこし内容が多すぎたかもしれません。今回は形態素解析と機械学習を利用して、著者の推定をしました。細かい説明は省略しましたので、興味があれば調べてみてください。ナイーブベイズは、迷惑メールの判定にも使用されることがある手法です。データを増やしてほかのモデルの使用を検討すると、代筆などが疑われている作品を数理的に評価することができるかもしれません。次回からはすこし音声を扱ってみるつもりです。それでは、次週の更新をお楽しみに!
注
(1) 著作権切れによるパブリックドメイン、もしくは著者が掲載許可した作品のテキストを入手できます(https://www.aozora.gr.jp/)。西洋の文学に興味のある読者は、Project Gutenberg(https://www.gutenberg.org)を利用して、英語などで同様のことを試してみるとよい練習になるでしょう。
(2) コードの実行速度を上げるために数を制限しているため、本来はより多くのテキストが必要です。
(3) 今回は新字新仮名で統一しましたが、旧字旧仮名で読める作品もあります。
(4) 標準ライブラリーにないものをインストールする際に使用できます。ノートブック環境では!を先頭につけます。
(5) https://numpy.org/doc/stable/user/quickstart.html
https://scikit-learn.org/stable/tutorial/index.html
(6) 学習手法によっては、不均衡データの場合は重みをつけたり、リサンプリングをしたりする必要があります。
(7) いずれかの確率が0の場合に全体の確率が0になってしまうため、スムージング処理が施されます。また、それぞれの単語が出てくる確率は独立である(単語には共起しやすさがない)ことをナイーブに仮定することによって、かけ算をしていることが名前の由来です。
-
- 2023年02月07日 『5. 自然言語と人工言語のはざまで―ことばの研究・教育での言語処理技術の利用―:英単語帳の作成 野口大斗(東京医科歯科大学ほか非常勤講師)』
-
英単語帳の作成
OCRとテキストファイルの保存
前回は正規表現と見出し語化について確認しました。今回は、テキストファイルを読み込んで、オリジナルの単語帳を作ることを目標とします。テキストファイルを処理できるようになると、ウェブ上に公開されていない英文でも取り扱えるようになります。
まずは、分析したい英文を用意してみてください。手元にあるもので構いません。電子化されていないものは、文字認識(OCR)を活用しましょう。分量が少なければ、スマートフォンでGoogle Lens(https://lens.google)を使用するのが便利です。メモ帳などのテキストエディタに貼りつけて、テキストファイル(.txt)として保存し、Google Driveにアップロードしてください。サンプルはtestという名前で、Steve JobsのStanford Universityの卒業式のスピーチの原稿を使用しています(1)。こちらにコード記入済みのノートブックを用意しておきますが、今回はファイルを適切にアップロードし、パスを必要に応じて書き換えないと動作しません。そのため、出力結果を一部表示したまま共有しますが、適切にコードを実行することで結果が書き換えられます。
テキストファイルの読み込み
第3回目に紹介したGoogle Driveのマウント方法を覚えているでしょうか。以下のコードをColaboratoryで実行すると、Google Driveをマウントすることができます。
#Google Driveをマウントする。
from google.colab import drive
drive.mount('/content/drive')
さて、パスを指定して、ファイルを読み込んでみましょう。Colaboratoryのファイルマークのところで指定するフォルダーを選び、右の3つの点と「パスのコピー」を順にクリックします。コード上に貼りつけて、パスを書き換えましょう。以下のコードでは、contentの中のdriveの中のMyDriveの中に、testという名前のtxtファイルがあることを前提として、パスを指定しています。ファイルを読み込んで、テキストを表示してみます。
#ファイルを読み込む。
file = open('/content/drive/MyDrive/test.txt')
#ファイルのテキストを取り出す。
text = file.read()
text
見出し語化
スペースごとに分割して、単語のリストを作成します。
#単語ごとにリストにする。
words = text.split()
words
結果を見てみると、句読点が残っていると思います。句読点などの記号を除去してみましょう。英語の記号で主なものはつぎのコードで表示できます。
#stringモジュールをインポートする。
import string
#句読点などの記号類を指定する。
string.punctuation
ここで、処理方法を確認しておきましょう。たとえば、abcdeからbとeを削除したい場合は、つぎのように書けます。maketransの1つ目の引数に置き換え前の文字を、2つ目に置き換え後の文字を、3つ目に削除する文字を渡すことになっています。それぞれの文字はつづけて書きますが、1文字ずつ解釈されます。置き換えの動作についても、あわせて確認しておくことにします。
#abcにおいてaをA、cをCに置き換え、bとeを削除する。
'abcde'.translate(str.maketrans('ac', 'AC', 'be'))
したがって、記号類を削除したい場合には、以下のようにします。置き換えは使用しないため、maketrans 1つ目と2つ目の引数は空欄にし、stringモジュールの句読点などの記号類を3つ目に指定します。
#単語リストのそれぞれの語の中で、句読点などの記号があるものはその記号を削除する。
words = [word.translate(str.maketrans('', '', string.punctuation)) for word in words]
words
英語では文頭が大文字になってしまいますが、文頭に出てきた語もそれ以外の位置に出てきた語も同一の語として集計したいので、すべての文字を小文字にします。固有名詞や一人称単数の代名詞などでは不都合ですが、今回は例外的な処理はおこないません。
#単語リスト中のすべての文字を小文字にする。
words = [word.lower() for word in words]
words
屈折接辞を除去し、語を原形に戻します。前回学んだ見出し語化を適用することで解決します。
#nltkをインポートし、必要なデータをダウンロードする。
import nltk
nltk.download('omw-1.4')
nltk.download('wordnet')
from nltk.stem import WordNetLemmatizer
#見出し語化する。
words = [WordNetLemmatizer().lemmatize(word) for word in words]
words
ストップワードの除去
ここで、前回の課題として残されていた機能語などの除去をしてみます。除去されるべき語(ストップワード)のリストはNLTKで用意されているものを使用します。
#ストップワードのリストをダウンロードする。
nltk.download('stopwords')
#英語のストップワードのリストを呼び出す。
nltk.corpus.stopwords.words('english')
['ab', 'cd']に含まれないものだけをプリントする場合は、以下のように書けます。inという演算子を使用すると、集合の中に特定の要素が含まれるかどうかを判断することができます。ここでは、集合の要素ではないものをプリントしたいので、notを使用して否定します。
#ab, bc, cdのうち、ab, cdの要素でないものだけをプリントする。
for item in ['ab', 'bc', 'cd']:
if item not in ['ab', 'cd']:
print(item)
したがって、ストップワードではない語だけをリストにする場合は、つぎのようになります。
#単語リストからストップワードを除去する。
words = [word for word in words if word not in nltk.corpus.stopwords.words('english')]
words
既知語の除去
さて、オリジナルの単語帳というからには、自分の知っている単語は除きたいでしょう。そこで、知っている単語をつぎのようにそれぞれの行に1つずつ書いたテキストファイルを作ります。
apple
banana
cherry
このファイルを英文テキストと同様の方法で読み込んでリスト化し、ストップワードと同じく除去します。本格的に既知語のリストを作る場合には、既存の単語のリストから知らない単語を除去するとよいでしょう。たとえば、「日本人大学生用英語基本語彙」のリスト(http://language.sakura.ne.jp/s/voc.html#:~:text=日本人大学生用英語基本語彙)であれば、スプレッドシートをダウンロードできます。知っている単語、もしくは知らない単語の横の列に任意の文字を入力しておき、ソートして既知語のみを選択し、テキストエディタに貼りつけるのが簡単でしょう。txtファイルとして保存して、Google Driveにアップロードすれば、準備完了です。
早速、読み込んでみましょう。Google Driveの変更が反映されていない場合には、Colaboratoryのマウント用アイコンの隣の更新用アイコンをクリックするか、再度Driveをマウントするコードを実行してみましょう。サンプルでは、仮に上位2000語が既知であるという想定のもと、my_vocab.txtという名前で、test.txtと同じ場所にアップロードしています。
#ファイルを読み込む。
file = open('/content/drive/MyDrive/my_vocab.txt')
#ファイルのテキストを取り出す。
my_vocab = file.read()
#既知語のリストを作る。
my_vocab = my_vocab.split()
my_vocab
ストップワードの場合と同様に、知っている語を除去します。
#単語リストから既知の単語を除去する。
words = [word for word in words if word not in my_vocab]
words
単語の出現回数をカウントしてみましょう。
#単語の出現回数を数える。
from collections import Counter
freq = Counter(words)
freq
数字や短縮形、edで終わる動詞などはうまく処理されていませんが、今回はコードを簡潔にするため、処理を省きます。見やすさのために、3回以上出現し、長さが7文字以上の語を抽出します。
#3回以上出現し、文字数が7以上である語のみ抽出し、targetという変数に格納する。
target = [word for word, frequency in freq.items() if frequency>=3 and len(word)>=7]
target
語義の付与
それぞれの語に意味を与えてみます。ここではPython上で完結させる方法を紹介しますが、Googleスプレッドシートのgoogletranslate関数(2)を使用したり、これから紹介するファイルとvlookup関数(3)を組み合わせたりする方法もあります。辞書はつぎのウェブサイトからテキスト形式のものをダウンロードして、zipファイルを展開してください。
https://kujirahand.com/web-tools/EJDictFreeDL.php
フォルダーの中のejdict-hand-utf8.txtのみをDriveにアップロードします。以前にCSV(コンマ区切り)ファイルを扱いましたが、今回はTSV(Tab-separated values, タブ区切り)ファイルです。pandasのread_tableを利用して読み込み、辞書形式にしてみましょう(4)。
#pandasをインポートする。
import pandas as pd
#read_tableを使用して、Driveにアップロードした英和辞書を読み込む。
df = pd.read_table('/content/drive/My Drive/ejdict-hand-utf8.txt', header=None)
#0番目の列と1番目の列をそれぞれセットにして、辞書形式のデータにする。
dictionary = dict(zip(df[0],df[1]))
dictionary
それでは、それぞれの語に意味を付与しておしまいにしましょう。辞書に登録されていない語や適切に見出し語化できていない語でエラーが出るのを防ぐため、プリントの前に辞書にあるかどうかを確認するようにしています。
#辞書に登録されていれば、語と意味をペアでプリントする。
for t in target:
if t in dictionary:
print(t, dictionary[t])
今回はここまでです。いかがだったでしょうか。ここまでに留めますが、NLTKのcmudictを用いて発音記号を付与したり、単語が使われている文を抜き出して例文としたり、単語リストを活用して語の難易度も付与したり、頻度順に並べ替えたり、複数の英文ファイルを使用してどのファイルで単語が出てきたのかを示したりすることもできます。また、学習者目線で単語帳を作りましたが、教材や試験の作成時に未修語を抽出したり、難易度を評価したりするのに使用することができます。次回はテキストの分類を取り扱います。それでは、次週の更新をお楽しみに!
注
(1) https://news.stanford.edu/2005/06/12/youve-got-find-love-jobs-saysのテキストを使用しました。ウェブ上に公開されているため、スクレイピングも技術的に可能です。
(2) https://support.google.com/docs/answer/3093331?hl=ja
(3) https://support.google.com/docs/answer/3093318?hl=ja
(4) 今回CSVファイルではなくTSVファイルを使うのは、語義の中にコンマが含まれているためです。
-
- 2023年01月31日 『4. 自然言語と人工言語のはざまで―ことばの研究・教育での言語処理技術の利用―:正規表現と見出し語化 野口大斗(東京医科歯科大学ほか非常勤講師)』
-
正規表現と見出し語化
正規表現とは
今回は正規表現について扱います。正規表現を使用すると、あいまいな文字列の集合を定義できるため、検索の自由度が広がります。読者のみなさんは、電子辞書を使用したことがあるでしょうか。紙の辞書しかなかった、スマートフォンで調べるから必要がないという世代の方が多いかもしれません。もし使用経験があれば、あいまい検索やワイルドカード検索といった機能を使ったことがあるでしょうか。たとえば、archaeologyの綴りの途中の母音字の順番がわからなくなったときに、arch〜logyなどのように検索すると、archから始まってlogyで終わる単語を表示してくれる機能です。Google検索(https://www.google.com)にも、単語単位であるものの、類似した機能があります。検索語句を二重引用符で囲むと、完全一致した結果が表示されますが、"natural * processing"のように検索すると、naturalとprocessingの間に語がはさまれたものが表示されます。今回はPythonでの正規表現の使用方法と正規表現を使用した語形変化の処理、実用的な語の見出し語化について紹介します。語形変化の処理に正規表現は十分ではないのですが、汎用性があるので、ぜひマスターしてください。今回も以下に入力済みのノートブックを用意しています。
Pythonでの正規表現
Pythonで正規表現(regular expression)を使用するには、reというライブラリーをインポートします。ここではsearchを使用します。すでに用意されているreモジュール内の関数を使うので、複雑な処理を簡単に書き表すことができます。検索する文字列と検索対象を順にコンマでつなぎます。任意の文字がいくつか入る場合は、.+
を用いて表します。先ほどのarch〜logyの例は、arch.+logy
と書けます。linguisticsという単語はこの条件を満たさないので、以下のコードを実行しても何も起こりません。
#reをインポートする。
import re
#linguisticsが条件arch.+logyを満たせばオブジェクトを返す。
re.search('arch.+logy', 'linguistics')
archaeologyは条件を満たすので、つぎのようにすると出力(オブジェクト)が返ってきます。
#archaeologyが条件arch.+logyを満たせばオブジェクトを返す。
re.search('arch.+logy', 'archaeology')
groupメソッドを使用すると、マッチした文字列が返ってきます。
#マッチした文字列を返す。
re.search('arch.+logy', 'archaeology').group()
たとえば、arch, archaeology, biologyの中で、arch.+logy
を満たすものだけプリントするには以下のようにします(1)。
#リスト中の単語のうち、arch.+logyを満たすものだけプリントする。
words = ['arch', 'archaeology', 'biology']
for word in words:
if re.search('arch.+logy', word):
print(re.search('arch.+logy', word).group())
動作が確認できたところで、メタ文字の使い方を少し眺めておきましょう。.+
を使用しましたが、.
は任意の1文字を表します。たとえば、a.c
にはabcやadcなどが一致します。
#aとcのあいだに任意の1文字が入る文字列にマッチさせる。
re.search('a.c', 'abc').group()
+
は直前の文字の1回以上の繰り返しを表します。この場合は、a+b
にはaabやaaabなどがマッチします。
#aの後ろでaが1回以上繰り返されcに続く文字列にマッチさせる。
re.search('a+c', 'aac').group()
したがって、任意の文字が1回以上現れることは、.+
を使用して表現できるのです。ほかにもよく使われるものの例を見てみましょう。0回もしくは1回以上の出現には?
を使います。a.?c
とすると、a.c
でマッチするものにくわえて、acにもマッチするようになります。
#acもしくはaとcのあいだに任意の1文字が入る文字列にマッチさせる。
re.search('a.?c', 'ac').group()
文字列の集合を表す場合には、[]
を用います。[abc]d
とすると、adやbd、cdにマッチします。
#[]の中の文字1つとdの場合にマッチさせる。
re.search('[abc]d', 'ad').group()
[]
の文字以外と一致させたい場合には、^
をつけます。
#[]の中の文字以外とdの場合にマッチさせる。
re.search('[^abc]d', 'dd').group()
半角の数字を検索してみましょう。全角の数字にはマッチしません。
#半角の数字にマッチさせる。
re.search('[0-9]+', 'ab0123').group()
半角数字に囲まれたひらがなだけを取り出してみましょう。しかし、以下のようにすると、数字も含まれてしまいます。
#半角数字とはさまれたひらがなを取り出す。
re.search('[0-9][ぁ-ん]+[0-9]', 'aあいbう1えお2').group()
つぎのようにかっこで分割して、その2つ目と指定するとひらがなだけを取り出すことができます。
#後方参照を利用して、半角数字にはさまれたひらがなのみを取り出す。
re.search('([0-9])([ぁ-ん]+)([0-9])', 'aあいbう1えお2').group(2)
ほかにもできることはたくさんあります。覚えるのは大変ですので、必要に応じて検索してみるとよいでしょう。
単語の見出し語化
さて、前回の語の頻度をカウントするコードを少し書き換えて実行してみましょう。後の説明の都合上、関数でまとめる範囲を小さくしています。また、見やすさのためにグラフではなく表の段階までにしています。
#URLの末尾を引数にしてスクレイピングしたテキストを返す。
def collect_texts(items):
import requests
from bs4 import BeautifulSoup
import time
text = ''
base = 'https://en.wikipedia.org/wiki/'
for item in items:
webpage = requests.get(base+item)
soup = BeautifulSoup(webpage.content, 'html.parser')
p_tags = soup.find_all('p')
for p_tag in p_tags:
text = text + p_tag.get_text().replace('\n', ' ')
time.sleep(1)
return text
#返ってきたテキストをtextに代入する。
text = collect_texts(['Language', 'English'])
#40回以上出現するもののをプリントする。
from collections import Counter
import pandas as pd
freq = Counter(text.lower().split())
df = pd.DataFrame.from_dict(freq, orient='index',columns=['frequency']).rename_axis('word')
df[df['frequency'] >= 40].sort_values('frequency', ascending=False)
ここで問題となったのは、複数形のsでした。さきほどはsearchを使いましたが、マッチした部分を置き換え(substitute)したいので、subを使用します。単語の末尾は$
で表しますので、sで終わる単語を削除したい場合は、sub('s$', '', (対象文字列))
となります。sを削除することによって、wordsをwordにしてみます。
#wordsのsを除去する
re.sub('s$', '', 'words')
これを使って書き換えてみましょう。
#語末のsを削除する。
freq = Counter([re.sub('s$', '', word) for word in text.lower().split()])
df = pd.DataFrame.from_dict(freq, orient='index',columns=['frequency']).rename_axis('word')
df[df['frequency'] >= 40].sort_values('frequency', ascending=False)
うまくいったように見えます。しかし、勘の鋭い方は、boxesのようなesの語が含まれる場合はどうするのかと思うでしょう。あってもなくてもよいものは、直後に?
をつけます。wordsでも動作することを確認してみてください。
#語末のesもしくはsを削除する。
re.sub('e?s$', '', 'boxes')
つぎに、countriesのような語が問題になるでしょう。iesの場合はyとすればよいわけですから、以下のようにします。
#iesをyに置き換える。
re.sub('ies$', 'y', 'countries')
これらを使用すると、規則的な複数形や動詞の3人称単数現在形を処理することができます。さらっと書きましたが、実は2行目と3行目の順序が重要です(2)。
#正規表現を利用して屈折変化のsを処理する。
words = [word.lower() for word in text.split()]
words = [re.sub('ies$', 'y', word) for word in words]
words = [re.sub('e?s$', '', word) for word in words]
freq = Counter(words)
df = pd.DataFrame.from_dict(freq, orient='index',columns=['frequency']).rename_axis('word')
df[df['frequency'] >= 40].sort_values('frequency', ascending=False)
反例
原理的にはこのように過去・過去分詞、現在分詞や比較などの活用を処理すれば、うまくいくように見えます。しかし現実問題としては、不規則形や屈折変化する前の形でマッチしてしまうもの、もとの語に屈折語尾に含まれる文字列がある場合に不都合が生じます。以下のコードで確かめてみてください(3)。
#正規表現による処理を確認してみる。
words = ['words', 'boxes', 'languages', 'countries', 'mice', 'tennis']
words = [re.sub('ies$', 'y', word) for word in words]
words = [re.sub('e?s$', '', word) for word in words]
words
見出し語化
結局、辞書を使わないと、うまく見出し語化はできません。ここでは、NLTK(Natural Language Toolkit)のWordNetLemmatizerを使用してみましょう。以下のように使うことができます。
#必要なものをインポートおよびダウンロードする。
import nltk
nltk.download('omw-1.4')
nltk.download('wordnet')
from nltk.stem import WordNetLemmatizer
#見出し語化する。
WordNetLemmatizer().lemmatize("words")
これを使うと、先ほどのものはつぎのようにうまく処理されます(4)。
#NLTKのWordNetLemmatizerを使用してみる。
words = ['words', 'boxes', 'countries', 'languages', 'mice', 'tennis']
words = [WordNetLemmatizer().lemmatize(word) for word in words]
words
それでは、前回のコードに見出し語化を適用してグラフまで描き、おしまいにしましょう。
#NLTKのWordNetLemmatizerで処理してからグラフを描く。
words = [word.lower() for word in text.split()]
words = [WordNetLemmatizer().lemmatize(word) for word in words]
freq = Counter(words)
df = pd.DataFrame.from_dict(freq, orient='index',columns=['frequency']).rename_axis('word')
df[df['frequency'] >= 40].sort_values('frequency', ascending=False).plot(kind='bar')
これで、前回指摘した問題のうちの1つが解決されました。見出し語化は正規表現だけでは理想的に解決できませんが、冒頭にも述べたように、汎用性のある技術です。条件を満たす単語や発音を調べたり、ファイルを検索したり、テキストから電話番号やメールアドレスのみを取り出したり、全角や半角に大文字・小文字などの表記のゆれを検出したりと発想次第で大活躍です。
次回は、機能語を除去し、実際に各自が選んだテキストから、知っている単語を含まないオリジナルの単語帳を作ってみます。すぐにそのまま使用できる即戦力のスクリプトです。それでは、次回の更新をお楽しみに!
注
(1) 何度も使用する場合は、同じ処理を繰り返さないようにコンパイルしておくとお行儀がよいです。
(2)このコードは期待通りのものを返します。words = ['arch', 'archaeology', 'biology']
pattern = re.compile('arch.+logy')
for word in words:
result = pattern.search(word)
if result:
print(result.group())
順序を入れ替えると(e)sが先に処理されてしまうので、うまくいきません。words = ['words', 'boxes', 'countries']
words = [re.sub('ies$', 'y', word) for word in words]
words = [re.sub('e?s$', '', word) for word in words]
words
(3) 見出し語化(レンマタイズ)のほかにステミングを呼ばれる処理もあり、辞書なしで速度を優先して処理する場合には、接辞をすべて取り除くという選択肢もあります。NLTKにも、ステマーが用意されています。words = ['words', 'boxes', 'countries']
words = [re.sub('e?s$', '', word) for word in words]
words = [re.sub('ies$', 'y', word) for word in words]
words
Porterステマー
Lancasterステマーporter = nltk.PorterStemmer()
[porter.stem(word) for word in ['words', 'boxes', 'languages', 'countries', 'mice', 'tennis']]
(4) より正確なレンマタイズには、統語解析と組み合わせることが必要です。lancaster = nltk.LancasterStemmer()
[lancaster.stem(word) for word in ['words', 'boxes', 'languages', 'countries', 'mice', 'tennis']]
-
- 2023年01月24日 『3. 自然言語と人工言語のはざまで―ことばの研究・教育での言語処理技術の利用―:CSVファイルとグラフによる言語データの可視化 野口大斗(東京医科歯科大学ほか非常勤講師)』
-
前回のコードは、最初のpタグのテキストのみではなくすべて取得するように修正しましたので、前回の部分のコードと実行結果が変わっています。
CSVファイルとグラフによる言語データの可視化
今回の流れ
今回は分析した結果を保存したり読み込んだりする方法、データの可視化、そして一連の作業をひとまとまりにする関数について紹介します。ファイルを扱う場合、Jupyter Notebookなどを使ってローカル(自分のコンピューター上)で作業をしていれば、ファイルのパスを指定するだけですが、Colaboratoryの場合にはGoogle Driveをマウントする作業を必要とします。まずは、CSVファイルから確認していきましょう。今回も入力済みのファイルを用意してあります。
CSVファイルとは
CSV(Comma-Separated Values)ファイルとはその名の通り、値(value)がコンマ(comma)で区切られた(separated)形式のテキストファイルです。表計算ソフトで開くと、Excelファイルと同じように値が表示されます。CSVファイルには値しか保存できませんが、値に関してはMicrosoft ExcelやGoogleスプレッドシートのような表計算ソフトでは同じように表示されます。メモ帳などのテキストエディタから開いてみると、エクセルファイルは人間には読めませんが、CSVファイルは表計算ソフトと同じ値がコンマで区切られているはずです。これがCSVの名前の由来です。それでは前回のコードを実行して、結果をCSVファイルとして保存してみましょう(1)。
前回のおさらい
前回のコードを実行してみましょう。このコードを実行することで、freqに頻度の情報が代入されます。
import requests
from bs4 import BeautifulSoup
import time
from collections import Counter
text = ''
base = 'https://en.wikipedia.org/wiki/'
items = ['Web_scraping', 'Robots.txt', 'Natural_language_processing']
for item in items:
webpage = requests.get(base+item)
soup = BeautifulSoup(webpage.content, 'html.parser')
p_tags = soup.find_all('p')
for p_tag in p_tags:
text = text + p_tag.get_text().replace('\n', ' ')
time.sleep(1)
freq = Counter(text.lower().split())
freq
Googleドライブのマウント
Googleドライブと連携させましょう。Google Colaboratoryの左端のバーにある、ファイルのマークのアイコンをクリックしてください。そうすると、開いた部分に4つのアイコンが出てきます。右から2つ目にある、カーソルをかざすと「ドライブをマウント」と表示されるものをクリックしてください。同意して自分のアカウントを選択し許可すると、ファイルに"drive"が追加されたことに気づくはずです。アイコンが見つからない場合は、つぎのコードをかわりに実行することもできます。これで、Googleドライブのマウントは完了です。
#Google Driveをマウントする。
from google.colab import drive
drive.mount('/content/drive')
データの保存
それでは、Googleドライブにデータを保存してみましょう。ここではpandasというライブラリーを使用して、freqの辞書データをdataframeという形式に変換してCSVファイルで保存します。まず、つぎのコードでデータの変換をおこないます。
#Pandasをインポートする。
import pandas as pd
#辞書データをdataframeに変換する。
df = pd.DataFrame.from_dict(freq, orient='index',columns=['frequency']).rename_axis('word')
df
データの数が多いので、20回以上出現する語に限定してみましょう。以下のように書きます。
#20回以上繰り返される語のみを抽出する。
df[df['frequency'] >= 20]
全体をCSVファイルで保存するには以下のようにします。ここでは、MyDriveに保存するようになっています。保存先を指定したい場合は、Colaboratoryのファイルマークのところで指定するフォルダーを選び、右の3つの点と「パスのコピー」を順にクリックします。パス(path)(2)とは道を意味する英単語で、フォルダーの階層構造がスラッシュでつながれたファイルの住所のようなものです。以下のコード中のパスは、contentの中のdriveの中のMyDriveの中にあるfrequencyという名前のcsvファイルということを示しています。コピーされているパスを貼りつけて、「/(ファイルの名前).csv」と追加すると指定先に保存が可能です。
#dataframeをcsvファイルでGoogle Driveに保存する。
df.to_csv('/content/drive/MyDrive/frequency.csv')
データの読み込みも簡単にできます。保存したばかりのファイルを、別の変数を使って読み込んでみましょう。もちろん保存先のパスから読み込むので、先ほど変更を加えた場合は、こちらも忘れずに変更してください。
#Google Driveのcsvファイルをdataframeとして読み込む。
df_1 = pd.read_csv('/content/drive/MyDrive/frequency.csv')
df_1
グラフの作成
通常、Pythonを用いてグラフを書く場合には、matplotlib(3)を使用します。しかし、pandasにはmatplotlibのラッパー(wrapper,包み込んで元とは違う環境で実行できるようにしたもの)があるため、簡単にグラフを描画することができます。今回は、棒グラフを作成してみましょう。スクレイピングした記事に20回以上出てくる語に限って可視化してみます。コードはたった1行でつぎのように書けます(4)。
#20回以上繰り返される語のみを抽出し、棒グラフにする。
df[df['frequency'] >= 20].plot(kind='bar')
頻度の多いものから順に並べることもできます。少ないものから並べる場合は、ascending=False
を削除するか、ascending=True
に書き換えます。
#20回以上繰り返される語のみを抽出し、降順に並べる。
df[df['frequency'] >= 20].sort_values('frequency', ascending=False)
プロットすると、以下のようになります。
#20回以上繰り返される語のみを抽出し、降順に並べて、棒グラフにする。
df[df['frequency'] >= 20].sort_values('frequency', ascending=False).plot(kind='bar')
これでグラフを作成することができました。plotのkindの値を変更するとグラフの種類を変更することもできます。また、凡例や軸についてはmatplotlibから設定できるので、興味があればウェブで検索して調べてみてください。
繰り返しを避ける(関数)
ここまでの手順をひとまとまり(関数)にして、繰り返しを避けましょう。まず、簡単な例として、与えられた数xに対して、3x^2+2x+1を計算してみましょう。例えば、[1,2,3]の場合はそれぞれつぎのようになります。
print(3*1**2+2*1+1)
print(3*2**2+2*2+1)
print(3*3**2+2*3+1)
なんだか打ち間違えそうです。高校の数学を思い出しましょう。関数を使って、以下のように表したはずです。(以下の関数は数学の例で、Pythonのコードではありません。)
f(x) = 3x^2+2x+1
f(1) = 6
f(2) = 17
f(3) = 34
これをPythonでコードにすると、つぎのようになります。defの後ろに関数名、かっこの中に変数(引数)を入れます。セミコロンとインデントを忘れないようにしましょう。関数の中身が複数行の場合は、すべてインデントします。今回は値を返したいので、returnを使用します。呼び出し方は、関数名のかっこの中に変数の値を代入するだけです。
#関数化する。
def f(x):
return 3*x**2+2*x+1
#関数に1から3までの値を代入する。
print(f(1))
print(f(2))
print(f(3))
それでは、本題に戻って、可視化の処理を関数にしてみましょう。
#スクレイピングから可視化までの流れを関数として定義する。
#itemにはリストでURLの末尾を、nには整数で単語の出現回数の閾値を受け取る。
def visualize(items, n):
import requests
from bs4 import BeautifulSoup
import time
from collections import Counter
import pandas as pd
text = ''
base = 'https://en.wikipedia.org/wiki/'
for item in items:
webpage = requests.get(base+item)
soup = BeautifulSoup(webpage.content, 'html.parser')
p_tags = soup.find_all('p')
for p_tag in p_tags:
text = text + p_tag.get_text().replace('\n', ' ')
time.sleep(1)
freq = Counter(text.lower().split())
df = pd.DataFrame.from_dict(freq, orient='index',columns=['frequency']).rename_axis('word')
df[df['frequency'] >= n].sort_values('frequency', ascending=False).plot(kind='bar')
このコードを一度実行しておくと、それ以降は簡単に呼び出すことができます。つぎのコードのように関数に引数を入れて呼び出すと、さきほどと同じグラフを描画できます。
#'Web_scraping', 'Robots.txt', 'Natural_language_processing'の記事で20回以上出てくる単語を棒グラフで可視化している。
visualize(['Web_scraping', 'Robots.txt', 'Natural_language_processing'], 20)
languageとEnglishの記事で40回以上出てくる語のように条件を変えても、短いコードで簡単に処理できます。
#'Language', 'English'の記事で40回以上出てくる単語を棒グラフで可視化している。
visualize(['Language', 'English'], 40)
さて、ファイルの読み込みと可視化、関数という今週の内容はおしまいです。ファイルを使えるようになるとできることの幅が大きく広がります。結果を保存できるだけでなく、既存のデータを読み込めるようになるからです。たとえば、最初に手作業で作成したCSVファイルも読み込むことができます。ファイルなしでは、単語の頻度や単語に含まれる文字数のカウントなどにできることが限られます。しかし、これからは既存のリストから語彙レベルを調べたり、既習語のリストを使って未習語を抽出したり、アイデア次第で挑戦できることが増えます。それらの一部については、この連載でも紹介予定です。
来週は正規表現を扱います。正規表現はあいまいな検索をする際に使用することができます。電子辞書やMicrosoft Wordなどで使用経験があるかもしれません。今回のグラフに問題があることに気づいた方も多いと思いますが、語形変化した単語が違うものとして扱われていて、冠詞や前置詞などの機能語が多く含まれています。正規表現を利用すると前者の問題を解決することができます。後者については、5週目以降に取り扱う予定です。それでは、来週の更新をお楽しみに!
注
(1) WindowsでShift-JISを使用している場合には、Excelなどで文字化けが起こることがあります。encodingを指定することも可能ですが、ファイルをUTF-8に統一することをおすすめします。比較的新しいExcelでは、保存の際にCSV UTF-8(コンマ区切り)を選べます。UTF-8のCSVファイルをExcelで読み込む際に文字化けが起こる場合は、「データ」-->「データの取得」-->「テキストまたはCSVから」として「65001: Unicode (UTF-8)」を選択して読み込むのがよいでしょう。
(2) Windowsを使用してJupyter Notebookなどでパスを指定する場合は、raw文字列の使用やエスケイプが必要になります。
(3) 日本語は文字化けする場合があります。japanize-matplotlibの使用を検討してみてください。
(4) 実行環境によっては、plt.show()が必要です。
-
- 2023年01月17日 『2. 自然言語と人工言語のはざまで―ことばの研究・教育での言語処理技術の利用―:Webテキストの利用(スクレイピング)と頻度のカウント 野口大斗(東京医科歯科大学ほか非常勤講師)』
-
Wikipedia記事の最初のpタグは本文の1段落目と一致しないことが多いため、pタグで囲まれたテキストの1つ目を取り出していたコードをすべて取り出すように書き換えました。(1/21)
Webテキストの利用(スクレイピング)と頻度のカウント
スクレイピングとその注意点
前回の内容はいかがだったでしょうか。基本的な話が中心だったため、あまり実践的ではなく、少し簡単だったかもしれません。今回はさっそく、すぐに活用できる技術についてお伝えします。前回は英文のテキストを手作業で貼りつけましたが、今回はウェブサイトから情報を抜き出す、スクレイピングという技術の紹介です。今回はURLが予測可能なタイプのウェブサイトの複数のページからテキストを収集し、それぞれの単語が出現する回数を数えることができるようになるということを目標に進めていきます。今回もPythonを使用します。
大まかな流れは、ウェブサイトのURLを生成する、URLを指定してページの情報を取得する、HTMLのタグを取り除く、単語の数を数えるというものになります。新たに紹介する主なこととしては、文字列の取り扱い、ライブラリーのインポート、HTMLの仕組みなどが挙げられます。
まずは、本題に入る前に注意点を確認しておきましょう。ウェブサイトをスクレイピングする際は利用規約を確認し、スクレイピングが許可されていても、サーバーに負担をかけないようにすることが重要です。サーバーへ高頻度のアクセスをすると、岡崎市立中央図書館事件(1)のように偽計業務妨害で罪に問われることがあります。スクレイピングの可否は、スクレイピングしたいウェブサイトのrobots.txtというページを確認します。今回はWikipedia(https://en.wikipedia.org)を使いますので、そのrobots.txtのページ(https://en.wikipedia.org/robots.txt)を確認します。本記事の執筆時点でWikipediaのrobots.txtは、"Friendly, low-speed bots are welcome viewing article pages"(友好的で低速であれば機械的なアクセスは歓迎される)となっています。ちなみにWikipediaで大量のテキストをダウンロードする場合は、全記事の専用ファイルが提供されている(2)ので、そちらを使うようにしましょう。
ウェブサイトの読み込み
実際にコードが動かないと面白くないので、説明は後回しにして、さっそくウェブページを読み込んでみましょう。Colaboratoryを開いてください。Google Driveで「新規」-->「その他」-->「Google Colaboratory」とクリック(「Google Colaboratory」がない場合は、「+アプリを追加」から追加)して開けます。前回と同じように、このリンク(https://colab.research.google.com/#create=true)を使っても構いません。今回も以下に入力済みのノートブックを用意しています。
つぎのコードを打ち込んでください。Wikipediaのウェブスクレイピングの記事(https://ja.wikipedia.org/wiki/ウェブスクレイピング)をスクレイピングしてみます。出力結果の見やすさのために日本語版を使用します。ひとまず、コードを実行してみましょう。
#requestsというライブラリーをインポートする。
import requests
#ウェブページを取得する。
#スペースの都合上、\を使用して折り返しているが、1行で書いても同じ。
webpage = requests.get('https://ja.wikipedia.org/wiki/'\
'%E3%82%A6%E3%82%A7%E3%83%96%E3%82%B9%E3%82%AF%E3%83%AC%'\
'E3%82%A4%E3%83%94%E3%83%B3%E3%82%B0')
#テキストを表示する。
webpage.text
HTMLの処理
なんだか人間には読めそうにない文字列ではあるものの、ページの内容を取得できたことが確認できるはずです。少し見やすくして、本文だけを表示してみましょう。
#BeautifulSoupというライブラリーをインポートする。
from bs4 import BeautifulSoup
#HTMLを解析する。
soup = BeautifulSoup(webpage.content, 'html.parser')
#本文を抽出する。
soup.find_all('p')
改行記号とタグが入っているという問題点以外は、本文がうまく表示されているはずです。以下のようにすると、本文から改行記号とタグは削除されます。
#改行記号とタグを削除する。
for text in soup.find_all('p'):
print(text.get_text().replace('\n', ''))
HTMLの仕組み
さて、ページをうまく読み込めたので、少しHTMLの仕組みについて解説しておきます。HTML(HyperText Markup Language)とは、テキスト(text)よりも上位な(hyper)、しるしをつける(markup)ための言語(language)です。テキストをどう表示するのかのタグをつけるのに使われます。ウェブページは、実際に見えているように書かれているのではなく、テキストにそれぞれの部分をどう表示するかというタグ(<x>...</x>)がつけられています。実際はどのように記述されているのか確認してみましょう。さきほどのWikipediaのスクレイピングに関する記事を、Pythonではなく、ウェブブラウザから開いてみてください。
https://ja.wikipedia.org/wiki/ウェブスクレイピング
環境によって操作が異なるので、ここではGoogle Chrome(3)を使用している前提で話を進めます。さきほどPythonから読み込んだページをブラウザ(Google Chrome)から開きます。画面上を右クリックすると、「ページのソースを表示」と表示されるので、それをクリックします。ウェブサイトは、ソースに表示されているように記述されています。最初のコードで読み込んだものが読みにくかったのは、そのせいです。ブラウザがこのソースを解釈して、普段見ている形式で表示してくれているのです。
実際のウェブサイトは複雑でわかりにくいので、少し簡単な例で確認してみましょう。つぎの文字列をテキストエディタに貼り付けて、拡張子をhtmlとして保存してみましょう。ここでは、Windowsでメモ帳(テキストエディタ)を使っていることを想定して説明します。
<h1>見出し1</h1>
<h2>見出し2</h2>
<b>太字</b>
<i>斜体</i>
<u>下線</u>
まず、メモ帳を開きます。アプリが見つからない場合は、タスクバーの検索欄から検索してみましょう。メモ帳を開いたら、上記のタグで囲まれた文字列を貼りつけます。「ファイル」-->「名前を付けて保存」の順にクリック、ここでは仮に保存場所にデスクトップを指定します。適当な名前をつけ、その後ろに".html"と打ち込みます。たとえば、ファイル名を"test"とすると、以下のようになります。
test.html
ファイルの種類を「テキスト文書(*.txt)」から「すべてのファイル(*.*)」に変更して保存します。保存ができたら、デスクトップ(指定した保存場所)を確認してみてください。testのファイルが保存されているはずです。ファイルが既定のウェブブラウザのアイコンになっている場合は、そのままアイコンをダブルクリックして、ファイルを開いてみましょう。そうでない場合は、アイコンを右クリックして、「プログラムから開く」をクリックし、ウェブブラウザを選択してください。つぎのように、それぞれの「見出し」(headline)が大きく、「太字」が太字(bold)、「斜体」が斜体(italic)、「下線」が下線(underline)つきで表示されているのが確認できるはずです。実際のhtmlドキュメントや文字コードの宣言などを省略しているので、ブラウザなどの環境によっては、うまく動作しない可能性もあります(4)。
見出し1
見出し2
太字 斜体 下線
BeautifulSoupでのHTMLの処理の仕組み
最初のrequestsを使ったコードでは、webpage.textでHTMLタグを含んだままのテキストを表示していたので、人間には読みにくかったのです。ウェブサイトの本文だけを表示するコードでは、テキストを読みやすくするために、BeautifulSoupを使ってHTMLを解析することで、必要な部分だけを取り出しました。さきほど作ったHTMLの例で、動作を再度、確認してみましょう。
#test_htmlという変数に自作したhtmlを代入する。
test_html = '<h1>見出し1</h1><h2>見出し2</h2><b>太字</b><i>斜体</i><u>下線</u>'
#test_htmlを解析する。
soup = BeautifulSoup(test_html, 'html.parser')
#<h1>...</h1>とマークアップされている部分を探して、その部分のテキストを取得する。
soup.find('h1').get_text()
太字の部分を探したい場合は、h1をbに書き換えます。
#<b>... </b>とマークアップされているものをすべて取得して、1つ目を表示する。
soup.find_all('h1')[0].get_text()
URLの生成
さて、「URLが予測可能なタイプのウェブサイトの複数のページからテキストを収集し、それぞれの単語が出現する回数を数える」という目標を達成してみましょう。単語の数を数える際に、日本語を取り扱うと複雑になるので、今回も英語を使用します。Wikipediaの英文記事のURLを3つ用意してみました。
https://en.wikipedia.org/wiki/Web_scraping
https://en.wikipedia.org/wiki/Robots.txt
https://en.wikipedia.org/wiki/Natural_language_processing
まず、URLの構造は、簡単に予測できると思います。"https://en.wikipedia.org/wiki/"の部分が共通で、項目名の先頭が大文字になり、スペースが入る場合はアンダースコア(_)に置き換わっているというぐあいです。前回学んだことを使うと、以下のように項目名をリストにして、順にプリントすることができます。
items = ['Web_scraping', 'Robots.txt', 'Natural_language_processing']
for item in items:
print(item)
文字列同士を結合するときには、足し算の記号を使います。
a = 'A'
b = 'B'
a + b
これを念頭におくと、URLはつぎのように生成できます。
base = 'https://en.wikipedia.org/wiki/'
items = ['Web_scraping', 'Robots.txt', 'Natural_language_processing']
for item in items:
print(base + item)
スクレイピングの実行
3つの記事の最初の段落の文章を結合してみましょう。
#必要なライブラリーをインポートします。
import requests
from bs4 import BeautifulSoup
import time
#空の文字列を用意しておきます。
text = ''
base = 'https://en.wikipedia.org/wiki/'
items = ['Web_scraping', 'Robots.txt', 'Natural_language_processing']
for item in items:
webpage = requests.get(base+item)
soup = BeautifulSoup(webpage.content, 'html.parser')
p_tags = soup.find_all('p')
for p_tag in p_tags:
text = text + p_tag.get_text().replace('\n', ' ')
#1秒間実行を停止させています。( )の中に停止させたい秒数を入れます。
time.sleep(1)
text
サーバーへの負担軽減
重要なのは、ループを回すごとに、動作を1秒間停止(sleep)させていることです(5)。sleepの動作についてだけ、簡単に確認しておきます。以下は、現在時刻を3回プリントするコードです。
#必要なライブラリーをインポートする。
import datetime
#3回ループさせるという意味である。
for i in range(3):
#現在時刻をプリントする。
print(datetime.datetime.now())
これにsleep(1)を挿入すると、1秒ずつ停止します。
import datetime
#timeというライブラリーも追加する。
import time
for i in range(3):
print(datetime.datetime.now())
#ループごとに1秒間停止させる。print文と字下げの深さがそろっていることに注意する。
#for文とそろえると、ループが終わってから1秒間停止する。
#そのため、アクセスの頻度を下げる役割を果たさなくなる。
time.sleep(1)
語の出現回数
さて、3つの記事を結合したtextに代入されているテキストに話を戻しましょう。前回のコードを使用すると、andが出てくる回数を数えることができます。
i = 0
for word in text.lower().split():
if word == 'and':
i = i+1
print(i)
今回は指定した文字だけではなく、すべての語の出現回数を求めようとしています。簡単な例['a', 'a', 'b']におけるそれぞれの文字の出現回数は、つぎのようにカウントできます(6)。
#ライブラリーをインポートする。
from collections import Counter
#リストの要素の出現回数をカウントする。
Counter(['a', 'a', 'b'])
textに代入されている文を前回と同じ要領でリスト化します。text.lower().split()
このリストにおける出現回数を数えるには、つぎのようにします。
from collections import Counter
Counter(text.lower().split())
変数に結果を代入しておきましょう。freq = Counter(text.lower().split())
すると、以下のようにすることで、先週のように任意の単語(ここではa)の出現回数を調べることができます。aの部分は調べたい単語で書き換えてみてください。freq['a']
これで、「URLが予測可能なタイプのウェブサイトの複数のページからテキストを収集し、それぞれの単語が出現する回数を数える」という目標は達成です。本当はそれぞれの単語を原形にしてカウントする必要があるのですが、それは今後の記事に譲ることにします。スクレイピングは実際に自分で選んだウェブサイトで試してみてください(7)。
実際にプログラムを使ってできそうなことのアイデアが、浮かんできたのではないでしょうか。たとえば、ページのソースでHTMLを確認すれば、ページ内の見出しだけを収集したり、リンクだけを収集してそのリンクのページをスクレイピングしたりすることなども可能です。プログラミングの入門書では、数値計算から始まるのがお約束ですので、2回目からスクレイピングというのは斬新だったかもしれません。しかし、コンピューター関連の情報は膨大ですので、すべて1度に学ぼうとするのではなく、まず興味のあるコードを動かしてみて、必要に応じて調べながら随時学んでいくのが、情報が目まぐるしく変化する社会では重要です。
また、プログラミングの面白いところは、小さな例から実験できる、何度やり直しても文句はいわれない、コピーさえとっておけば、自分のコンピューター内の状況は、いつでももとに戻すことができるということです。そして、自分の頭の中にあるものを実際の形にすることができます。失敗をおそれずに、どんどんコードを書き換えてみてください。うまく動かなくなったら、このページから再度コードを取得すれば問題ありません。今回のスクリプトでは、ウェブサイトにアクセスします(自分の外の世界に影響を与えます)ので、その時だけ注意を払ってください。そうはいっても、必要なのは、アクセスしてよいサイトか確認をすること(ブログラムが書けるのかどうかに関係がない能力)と、早くアクセスしすぎていると感じたらPythonを終了する、インターネットから切断する、コンピューターをシャットダウンしてやり直すといった基本的な対応だけです。
次回は単語の頻度をグラフにしてみます。Google Driveと接続して、結果をファイルで書き出す方法や同じコードの繰り返しを避ける方法(関数)などについても紹介します。それでは、来週の更新をお楽しみに!
注
(1) https://ja.wikipedia.org/wiki/岡崎市立中央図書館事件
(2) https://ja.wikipedia.org/wiki/Wikipedia:データベースダウンロード
(3) https://www.google.com/chrome
(4) うまく動作しない場合は、以下のものを試してみてください。
(5) robots.txtに“Hits many times per second, not acceptable”(1秒間に何度もアクセスするのは許されない)とあるので、1秒に1回に制限しています。アクセス先のウェブサイトに合わせて、調節してください。<!DOCTYPE html>
<html lang=”ja”>
<head>
<meta charset="UTF-8">
<title>My test page</title>
</head>
<body>
<h1>見出し1</h1>
<h2>見出し2</h2>
<p><b>太字</b><i>斜体</i><u>下線</u></p>
</body>
</html>
(6) 本文中では省きましたが、辞書というタイプのデータは、つぎのように使用できます。
また、コードを単純にするためにライブラリーを使用しましたが、単語のカウントは辞書を用いて、以下のようにもおこなえます。#空の辞書を用意する。
dictionary = {}
#appleという見出しの値を1にする。
dictionary['apple'] = 1
#現状のdictionaryの中身をプリントする。
print(dictionary)
#bananaという見出しの値を2にする。
dictionary['banana'] = 3
#最終的なdictionaryの中身をプリントする。
print(dictionary)
(7) JavaScriptが使用されているページをスクレイピングする際には、seleniumなどのライブラリーが必要になります。JavaScriptが使用されているか確認するには、Google Chromeで右上の3つの点の部分をクリックし、「設定」-->「プライバシーとセキュリティ」-->「サイトの設定」-->「JavaScript」-->「サイトに JavaScript の使用を許可しない」を選択し、ページを再読み込みしましょう。この設定で閲覧できなくなった部分に関しては、JavaScriptが使われています。多くのウェブサイトが正常に動作しなくなるため、確認が終わった後は、「サイトが JavaScript を使用できるようにする」に戻し、ページを再読み込みしてください。freq_letter = {}
for letter in ['a', 'a', 'b']:
if letter not in freq_letter:
freq_letter[letter] = 1
else:
freq_letter[letter] = freq_letter[letter]+1
freq_letter
-
- 2023年01月10日 『1. 自然言語と人工言語のはざまで―ことばの研究・教育での言語処理技術の利用―:ことばを取り巻く環境とPythonの動作確認 野口大斗(東京医科歯科大学ほか非常勤講師)』
-
自然言語と人工言語のはざまで
この連載では、言語の研究と教育におけるテクノロジーの活用について扱います。自然言語(人間のことば)に興味のある方、人工言語(プログラミング言語)を学び始めた方を念頭に執筆しています。具体的には、プログラミングを学び始めた中高生、日本語(国語)や英語を教えている方、情報の授業のプログラミングの題材を探している中高の教員、プログラミングを活用したい言語の研究に関わっている学生や研究者、ことばに関心がありプログラミングを始めてみたい方を想定しています。プログラミングや自然言語処理の知識のある方には十分な内容となっていないことをあらかじめお断りいたします。初回ですので、わたしたちのことばを取り巻く環境についてすこし前置きをしてから、本題に入りたいと思います。
(本連載では、主にPythonを利用します。Googleのアカウントを持っていることを前提として話を進めますので、アカウントがない場合にはぜひ新規作成してコードを実行してみてください。コードは、Windows 11上のGoogle Chromeでの動作を確認しています。コードの確認には万全を期していますが、利用結果にともなう責任は負いません。)
ことばを取り巻く環境
ことばのゆくえ
まず、外国語と関連するものとして、機械翻訳(Google翻訳やDeepLなど)は、多くの読者のみなさんにとって親近感があるのではないでしょうか。意識的に使っていなくても、ブラウザやサイトの設定で、ウェブページが自動的に翻訳されることさえあります。音声認識の精度も向上し、YouTubeやPowerPointの自動生成の字幕や同時翻訳の字幕を見たことがある方も多いはずです。文字認識技術の発展も驚異的で、タイプされた文字であれば、ほぼ誤りなく認識され、Google Lensなどはカメラをかざすだけで、即座に文字を翻訳してくれます。そんなものは使ったことがないと言われる方も、街中で知らず知らずのうちに、翻訳された文やそれが読み上げられた合成音声を、目にしたり耳にしたりすることくらいはあるはずです。それほどに、言語処理の技術はわたしたちの生活に浸透しています。
言語処理の技術は、外国語に限ったことではありません。母語であったとしても(わたしの場合は日本語ですが)、知らず知らずのうちに、わたしたちはテクノロジーに依存しています。たとえば、この原稿をタイプしながら、かな漢字変換システムを利用しています。現在では、文節ごとに変換して候補を吟味しなくても、うまくいくことが多いですし、個人の変換傾向に合わせたカスタマイズまでされるようになっています。メモ帳のようなテキストエディタではなく、高級なアプリケーションを使っていると、文章校正までおこなわれ、修正候補が提示され、設定によっては自動修正までおこなわれます。スマートフォンやタブレットでは、予測候補が表示され、もはや自分で文を紡ぎ出しているのかどうかさえ怪しくなってきているのが、昨今の状況です。
さらに、2022年の年末にChatGPTが話題になったことは、記憶に新しいかもしれません(https://chat.openai.com)。システムがことばを自動的に生成することもできます。機械が人間と対話してタスクをこなすということも驚くべきことです。通常の作文だけではなく、歌詞や参考文献を含むレポートを書いたり、意図的に間違いをくわえたりすることさえ、お手のものです。人間に特有であったはずの能力―ことば―は、いったいどこへいってしまうのでしょう。
ひとつの答え―積極的な活用
あまり悲観的になる必要はないのではないでしょうか。少なくとももうしばらくのあいだは、人間はコミュニケーションに言語を使いつづけるでしょうから、根本的に言語が人間の手を離れることはないと想像されます。また、機械学習に使われるデータは基本的にこれまでに人間が蓄積してきたものであるため、機械の学習システムは人間のそれと大きく変わらないはずです。わたしがいま書いている文も、どこかで誰かが使ったのを読んだり聞いたりしたことのある構文と語の可能な組み合わせのうちの1つにしかすぎないのと同じです。
ただし、人間と機械の大きな違いは、機械は人間が死ぬまでかかっても読み切れないほどのデータを、学習していることです。そして、機械は学んだことを忘れません。したがって、1人の人間よりも能力が高いのは、当然のことなのです。目くじらをたてることはありません。歴史に名を残すような偉人を除いて、多くの人間の日常の営み(少なくとも言語活動)はさほど創造的ではなく、機械に模倣されるということが明らかになっただけなのです。
では、わたしたちにできることは何なのでしょうか。わたしにとって、この問題へのアプローチは極めてシンプルです。積極的にテクノロジーを活用するのです。日常生活を思い浮かべてみてください。文書はコピー機を使わずに手で写すべきだ、車のギアはATではなくてMTでないといけない、単語の意味は紙の辞書で調べないといけないと主張するのは少数派でしょう。しかし、少し話をハイテクにすればどうでしょうか。書類ではなくてフォームで電子データにすべきだ、運転は自動化すべきだ、日常的に使わないのであれば実用的には外国語は機械翻訳で十分だ。これらについては、まだ意見がわかれるのではないでしょうか。
自動化の誤解
これらに共通するのは、人間のやることが簡単になるのではなく、まったくなくなるということです。事務、運転、外国語の仕事がなくなるからという抵抗にあって、自動化は前に進みにくいのです。(誤解のないように書いておくと、わたしは収入の大部分を外国語教員という職に依存していますし、実用性だけが外国語の効用だとは思いません。)そうだとしても、自動化できることを自動化するのはよいことのはずです。電話交換手やタイピストが必要だという意見は少ないでしょう。時代とともに、人間がやらないといけないことは減ってきています。
もちろん、自動化されて仕事から解放されるのは、悪いことではありません。それで生活できなくなることが問題なのです。たとえば、自宅の掃除が自動化されても、だれも文句は言いません。むしろ対価を払ってでも、ルンバなどの掃除ロボットを買い求める人もいます。家事に収入が発生しないからです。自動化と収入の問題は、切り分けて議論しなくてはならないのです。
テクノロジーの可能性
そうであれば、言語の処理の一部を選択的に自動化することはよいことのはずです。世の中に存在するテキストの量は、確実に増え続けていきます。すでに、人間がすべてを読むことは不可能です。検索や要約の処理ができるのはすばらしいことです。人間が生涯に学習できる言語の数にも限りがあります。機械翻訳の進歩によって、読めるテキストの範囲が増えるのも望ましいことです。機械で文章を自動生成できることが広く知られたら、長く書くことの意義も薄れ、文書作成からも解放されるでしょう。
ただし、既存の提供されているシステムだけでできることには限りがあります。研究や教育にぴったり合ったプログラムはないか、あっても有償であることがほとんどです。テクノロジーを最大限に活用するには、コードの仕組みを知っている必要があります。ChatGPTなどで自然言語からコードを生成することも可能になっているので、データを整形したり、スクリプトを組み合わせたりすることさえできれば、自然言語処理技術の活用の幅は大きく広がるはずです。言語処理の技術を開発するのは容易なことではありませんが、それを言語の研究や教育に利用することはそれほど難しいことではありません。それでは、全12回の連載で、自然言語と人工言語のはざまをさまよう旅にご案内します。ぜひ最後までおつき合いください。
Pythonの動作確認
Google Colaboratory
今回は練習として、テキストに特定の単語が現れる回数を数えてみます。日本語も取り上げる予定ですが、しばらくはプログラム上での扱いやすさのために、英語の例を使用します。主にPythonというプログラミング言語を使いますが、環境設定の手間を省くため、この連載ではGoogle Colaboratoryを使用します。出力結果は表示しませんので、実際に実行して確認してください。コードはGoogle側で実行されるため、コンピューターへの負荷はかかりません。スペックを気にする必要がないことも、Google Colaboratoryを使う利点です。
準備は、以下のリンクをクリックして、Googleにログインするだけです。すぐにコードを打ち込めるようになります。ファイルはColab Notebooksというドライブ上のフォルダー内に保存されます。
https://colab.research.google.com/#create=true
まずは、実際にこのページで紹介しているコード(字体の変わっている部分)を打ち込んでみることをおすすめしますが、以下のすでに入力済みのものからすぐに実行することもできます。実行する際に警告画面が出ます。また、1つ目のコードの実行には、接続のために少し時間がかかります。スマートフォンからでも実行できます。再生(実行)ボタンはカーソルをかざさないと出てこないため、スマートフォンの場合はコードの横の[ ]の部分をタップしてください。こちらからの実行内容は保存されません。保存したい場合は、「ファイル」-->「ドライブにコピーを保存」と操作して、コピーしたものを使用してください。
テキストの読み込み
テキストはファイルから読み込むのが理想的ですが、ColaboratoryにGoogle Driveをマウントする必要があります。また何らかのデータセットから、テキストをインポートする魔法の呪文を提供することも可能です。しかし、わかりやすさのために、今回はコードにテキストを直接貼りつけます。また、コードの簡潔さのために説明を省いたり、お行儀の悪いコーディングをしたりします(1)が、ご了承ください。コードはセルという枠の中に、以下のように記述します。コード内の#に続く、もしくは3つのシングルクオーテーションで囲まれた日本語の文字列はコメントのため不要です。また、コメント以外の部分に全角文字を入れるとエラーが出るので、注意してください。
'''テキストに説明文を代入する。
イコールの前後のスペースはあってもなくてもよい。'''
text = 'Python is an easy to learn, powerful programming language. It has efficient high-level data structures and a simple but effective approach to object-oriented programming. Python’s elegant syntax and dynamic typing, together with its interpreted nature, make it an ideal language for scripting and rapid application development in many areas on most platforms.'
ここでは、textという変数に、Pythonのチュートリアル(2)の説明文の1段落目を代入します。コードはできるだけ、コピーではなく打ち込んでみてください。(もちろんチュートリアルの説明文はコピーで構いません。)文字列としてチュートリアルの説明文を代入したいので、引用符を忘れないようにしてください。Pythonではシングルクオーテーションでもダブルクオーテーションでも問題ありません。コードの左の三角形の再生ボタンを押して、セルを実行してみましょう。これで代入が完了です。
つぎに、「+コード」を押して、新たなセルを作り、次のコードを打ち込んで、セルを実行してください。「+テキスト」ではないことに注意してください。「テキスト」は「コード」以外の情報を書き込む部分です。textに文字列が代入されていることを確認できます(3)。
#textの中身をプリント(表示)する。
text
単語への分割
つぎにコードを少し変えて、実行してみましょう。
#textを小文字にする。
text.lower()
これで、textに代入されている文字すべてが小文字(lower-case letters)になります。大文字で書かれている場合も小文字で書かれている場合も同じ単語として扱いたいので、この処理を施します。
さらに変更して、単語ごとに分割(split)してみましょう。
#textを小文字にして分割する。
text.lower().split()
リストという四角いかっこで囲まれてコンマで区切られたものが出力されます。ピリオドやコンマが不要ですが、未習事項(4)を使わないと解決できないので無視します。
ループ
繰り返しの操作について確認しましょう。以下の例では、リストの中のものを順に取り出して、プリントしています。コロンを忘れないようにしてください。Pythonでは字下げの位置関係が、コードの階層構造を示します。上から入力すると自動で字下げしてくれますが、Tabキーで字下げし、Shiftキー+Tabキーで字下げを元に戻すこともできます。
'''inの後ろのリストの要素の数の分だけループを回し、
リストの要素を順にforの後ろのwordに代入する。
wordはほかの変数名に変えてもよいが、
その場合は後続する変数も忘れずに変更する。'''
for word in ['a', 'b', 'c']:
#wordをプリントする。
print(word)
さきほどの単語のケースに置き換えると、ループを使ったコードはつぎのようになります。
#小文字化されたテキストのそれぞれの単語をプリントする。
for word in text.lower().split():
print(word)
条件分岐
簡単な例を使って、bだけをプリントする例を示します。今回はifが入れ子になったので、さらに字下げをします。代入はイコール1つですが、等しいかどうかの判定は2つになるので注意してください。
#リストのそれぞれの要素について同じ処理を繰り返す。
for word in ['a', 'b', 'c']:
#もしwordに入っている要素がbであれば、以下の処理を実行する。
if word == 'b':
#wordの中身をプリントする。
print(word)
実際の単語のケースで、さきほどのコードの判定する要素をandに置き換えると、つぎのようになります。
#小文字化された単語それぞれについて同じ処理を繰り返す。
for word in text.lower().split():
#もしwordに入っている単語がandであれば、以下の処理を実行する。
if word == 'and':
#wordの中身をプリントする。
print(word)
カウント
andが出現する回数を数えてみましょう。iという変数を用意して、0を入れておきます。andが見つかった場合に、iに1をくわえていくことにしましょう。
#iという変数に0をセットする。
#counterなど別の名前にしてもよい。
i = 0
for word in text.lower().split():
if word == 'and':
#単語がwordであった場合だけに1を追加する。
i = i+1
#複数の要素をプリントしたい場合は、コンマでつなぐ。
print(i, word)
andの出現回数だけ表示したい場合は、つぎのようにします。print文の字下げの深さが変わっているのに注意してください。
i = 0
for word in text.lower().split():
if word == 'and':
i = i+1
'''さきほどとは字下げの位置が変わり、
すべての繰り返しが終わってからiをプリントしていることに注意する。'''
print(i)
さて、長くなりましたが、今回はこのあたりでおしまいにしましょう。次回は指定したURLからウェブ上のテキストを自動で取り出して、それぞれの単語が出現する回数を数えてみることにします。ウェブからテキストを取り出す技術は、情報収集や自らコーパスを作成する際に活用可能です。連載の中盤では、語数の数え方を応用して、自分で指定した英文から自分の知っている単語を除いた、オリジナルの英単語帳を作るコードを紹介します。テキストの難易度を評価したり、未習語を洗い出したりする作業にも用いることができます。それでは、来週の更新をお楽しみに!
注
(1) たとえば、変数を極端に減らしたり、メソッドや副作用の説明を省略したりしています。Pythonについて興味がある場合は、入門の書籍やウェブサイト、動画、公式チュートリアルなどを参照することを推奨します。
(2) https://docs.python.org/3/tutorial
(3) ColaboratoryやJupyter Notebookではprint文がなくてもprintされるので省きますが、実行環境によってはprint(text)
としてください。
(4) リストをループで一度展開して、stripを使用したものを別のリストにappendするか、つぎのようにリスト内包表記を利用することができます。[word.strip('.').strip(',') for word in text.lower().split()]
-
- 2023年01月03日 『1月からの新連載のお知らせ』
-
10月から12回にわたり、「中国のことばの森の中で―武漢・上海・東京で考えた社会言語学」というタイトルで、新型コロナウイルス感染症の拡大で近くて遠い国になってしまった中国を社会言語学の観点から描いてくださった河崎みゆき先生、ありがとうございました。目の前に街並みが浮かんでくるような筆致で中国のことばの森を描写してくださいました。
さて、1月10日からの12回の連載は野口大斗(東京医科歯科大学ほか非常勤講師)による「自然言語と人工言語のはざまで―ことばの研究・教育での言語処理技術の利用―」です。言語の音やテクノロジーを活用した言語の研究や教育に関心があります。この連載では、Pythonなどで言語処理の技術を研究や教育にどのように利用できるかについてみなさんと考えていきます。(野口)