はじめに
もしかして 「え?MATLAB で言語処理やるの??」と思いました・・?(2回目)
先日 言語処理 100 本ノック 2020 が公開されましたが、これは言語処理という切り口で MATLAB の練習をするすごく良い題材じゃないか・・ということで第 1 章: 準備運動 に引き続き第 2 章の MATLAB コード例を公開します。
ノックを一緒にやってくれる MATLAB 芸人は引き続き募集中です!GitHub: NLP100-MATLAB
他章へのリンク
- 第 1 章: 準備運動
- 第 2 章: UNIX コマンド(この記事)
- 第 3 章: 正規表現
- 第 4 章: 形態素解析
- 第 6 章: 機械学習 part 1
- 第 6 章: 機械学習 part 2
- 第 7 章: 単語ベクトル
- 第 8 章: ニューラルネットワーク
UNIX コマンドの章ではありながら・・
第 2 章は本来 UNIX コマンドの課題ではありますが、すでによい解説付きの投稿が複数見られるので、MATLAB での処理に注力しています。
Windows 上で環境構築するのが大変そうだからではないです。
環境
MATLAB R2020a (Windows 10)
この記事の Livescript 版(MATLAB)は GitHub: NLP100-MATLAB1 に置いてあります。同じ課題を実現する方法は沢山ある思います。もしもっといい(効率がいい、面白い)方法があれば是非コメントください。
第2章: UNIX コマンド
popular-names.txtは,アメリカで生まれた赤ちゃんの「名前」「性別」「人数」「年」をタブ区切り形式で格納したファイルである.以下の処理を行うプログラムを作成し,popular-names.txtを入力ファイルとして実行せよ.さらに,同様の処理をUNIXコマンドでも実行し,プログラムの実行結果を確認せよ.
以下では popular-names.txt
は本スクリプトと同じフォルダに保存されているとします。
10. 行数のカウント
行数をカウントせよ.確認にはwcコマンドを用いよ.
readtable
関数で読み込むのが手っ取り早い。念のためヘッダー情報は変数名に使用しない設定にしておく。
tabletxt = readtable('popular-names.txt','ReadVariableNames',false);
height(tabletxt)
ans = 2780
2780 行ですね。もう少し low-level な関数だと fileread
関数でも行ける?全部読み込んで、splitlines
関数で改行箇所で分割します。
tmp = fileread('popular-names.txt');
txt = splitlines(tmp);
length(txt)
ans = 2781
一行多い・・これは最後に空の文字列が入ってしまうため。
txt(end)
ans =
{0x0 char}
Unix コマンドを使えるようにする設定準備するのがあれだったので、Windows OSのコマンドプロンプト上でできるもので代用すると・・
!find /v /c "" < popular-names.txt
2780
11. タブをスペースに置換
タブ1文字につきスペース1文字に置換せよ.確認にはsedコマンド,trコマンド,もしくはexpandコマンドを用いよ.
string
型にして replace
関数を使うのが簡単そう。
tmp = fileread('popular-names.txt');
txt = string(splitlines(tmp));
newtxt = replace(txt,"\t"," ");
newtxt(1:2)
ans = 2x1 string
"Mary→F→7065→1880"
"Anna→F→2604→1880"
あれ、タブ?が残っている・・・。なんだこの矢印は。文字コード確認してみます。
まず char 型にして5文字目。
tmp = char(txt(1));
char5 = tmp(5)
char5 = ' '
double(char5)
ans = 9
文字コード 9 はタブですね。char(9)
を string
型にして使ってみます。
newtxt = replace(txt,string(char(9))," ");
newtxt(1:2)
ans = 2x1 string
"Mary F 7065 1880"
"Anna F 2604 1880"
今度はうまくいきました。タブを他からコピペしてもうまくいきますが、MATLAB 上でタブキーを押すとスペース4つ分(自分の環境では)がタイプされるのでうまくいきません・・。
newtxt = replace(txt," "," ");
newtxt(1:2)
ans = 2x1 string
"Mary F 7065 1880"
"Anna F 2604 1880"
要調査項目。
12. 1列目をcol1.txtに,2列目をcol2.txtに保存
各行の1列目だけを抜き出したものをcol1.txtに,2列目だけを抜き出したものをcol2.txtとしてファイルに保存せよ.確認にはcutコマンドを用いよ.
table
型で読み込めば(readtable
)すぐですが、あえて fileread
関数でやります。
tmp = fileread('popular-names.txt');
txt = string(splitlines(tmp));
最後に残る空文字列を決して、split
関数で分割する。
txt(strlength(txt)==0) = [];
newtxt = split(txt);
newtxt(1:2,:)
ans = 2x4 string
"Mary" "F" "7065" "1880"
"Anna" "F" "2604" "1880"
各文字が要素として入った 2780 x 4 の string 配列となります。
1 列目を col1.txt に、2 列目を col2.txt に保存します。
writematrix(newtxt(:,1), 'col1.txt');
writematrix(newtxt(:,2), 'col2.txt');
13. col1.txtとcol2.txtをマージ
12で作ったcol1.txtとcol2.txtを結合し,元のファイルの1列目と2列目をタブ区切りで並べたテキストファイルを作成せよ.確認にはpasteコマンドを用いよ.
readcell
関数で読み込んで、string
型に変換。
col1 = string(readcell('col1.txt'));
col2 = string(readcell('col2.txt'));
結合は join
関数で。デフォルトでスペース1つ空けて結合されるので、ここはタブを指定します。"\t"
が使えないのは要調査。
col12 = join([col1,col2],char(9));
writematrix(col12, 'col12.txt');
14. 先頭からN行を出力
自然数Nをコマンドライン引数などの手段で受け取り,入力のうち先頭のN行だけを表示せよ.確認にはheadコマンドを用いよ.
prompt = 'What is your N value? ';
N = input(prompt);
if isnumeric(N) && N == round(N) && N > 0
tmp = fileread('popular-names.txt');
txt = string(splitlines(tmp));
txt(1:N)
else
disp('N must be a natural number.')
end
ans = 5x1 string
"Mary→F→7065→1880"
"Anna→F→2604→1880"
"Emma→F→2003→1880"
"Elizabeth→F→1939→1880"
"Minnie→F→1746→1880"
(出力はコマンド入力で 5 を渡した場合)
15. 末尾のN行を出力
自然数Nをコマンドライン引数などの手段で受け取り,入力のうち末尾のN行だけを表示せよ.確認にはtailコマンドを用いよ.
prompt = 'What is your N value? ';
N = input(prompt);
if isnumeric(N) && N == round(N) && N > 0
tmp = fileread('popular-names.txt');
txt = string(splitlines(tmp));
txt(end-N-1:end-1)
else
disp('N must be a natural number.')
end
ans = 6x1 string
"Oliver→M→13389→2018"
"Benjamin→M→13381→2018"
"Elijah→M→12886→2018"
"Lucas→M→12585→2018"
"Mason→M→12435→2018"
"Logan→M→12352→2018"
出力はコマンド入力で 5 を渡した場合。最後の空文字の取り扱いが苦しい・・。
16. ファイルをN分割する
自然数Nをコマンドライン引数などの手段で受け取り,入力のファイルを行単位でN分割せよ.同様の処理をsplitコマンドで実現せよ.
N 分割・・。割り切れない分は最後で吸収します。
prompt = 'What is your N value? ';
N = input(prompt);
if isnumeric(N) && N == round(N) && N > 0 && N < 2780
tmp = fileread('popular-names.txt');
txt = string(splitlines(tmp));
Ndata = ceil(2780/N); % number of rows on each file (except for the last)
idx = reshape(1:(Nfiles-1)*Ndata, Ndata, []);
for ii=1:N-1
writematrix(txt(idx(:,ii)), "data"+ii+".txt");
end
writematrix(txt(idx(end)+1:end), "data"+N+".txt");
else
disp('N must be a natural number and smaller than 2780.')
end
上の idx
は各ファイルに収まる行番号が入るようになっています。
- 1列目:1つ目のファイルに収まる行数(N = 100 であれば 1 から 100)
- 2列目:2つ目のファイルに収まる行数(N = 100 であれば 101 から 200)
- 3列目:3つ目のファイルに収まる行数(N = 100 であれば 201 から 300)
といった具合。その行番号が入った配列で元データの中身を参照しながら各ファイルへ出力しています。
17. 1列目の文字列の異なり
1列目の文字列の種類(異なる文字列の集合)を求めよ.確認にはcut, sort, uniqコマンドを用いよ.
そろそろ MATLAB らしく処理していいよね・・。
tabletxt = readtable('popular-names.txt','ReadVariableNames',false);
せっかくなので変数名を付けておきましょう。日本語文字が使えるのは R2019b から。
tabletxt.Properties.VariableNames = ["名前","性別","人数","年"];
uniqueNames = unique(tabletxt(:,1));
head(uniqueNames)
名前 | |
---|---|
1 | 'Abigail' |
2 | 'Aiden' |
3 | 'Alexander' |
4 | 'Alexis' |
5 | 'Alice' |
6 | 'Amanda' |
7 | 'Amelia' |
8 | 'Amy' |
18. 各行を3コラム目の数値の降順にソート
各行を3コラム目の数値の逆順で整列せよ(注意: 各行の内容は変更せずに並び替えよ).確認にはsortコマンドを用いよ(この問題はコマンドで実行した時の結果と合わなくてもよい).
3コラム目ということは「人数」順ですね。逆順ということは多い順(descent)かな?
tabletxt = readtable('popular-names.txt','ReadVariableNames',false);
tabletxt.Properties.VariableNames = ["名前","性別","人数","年"];
sort
関数で並べ替えた順番を表すインデックス idx
を使います。
[~,idx] = sort(tabletxt{:,3},'descend');
tabletxt = tabletxt(idx,:);
head(tabletxt)
名前 | 性別 | 人数 | 年 | |
---|---|---|---|---|
1 | 'Linda' | 'F' | 99689 | 1947 |
2 | 'Linda' | 'F' | 96211 | 1948 |
3 | 'James' | 'M' | 94757 | 1947 |
4 | 'Michael' | 'M' | 92704 | 1957 |
5 | 'Robert' | 'M' | 91640 | 1947 |
6 | 'Linda' | 'F' | 91016 | 1949 |
7 | 'Michael' | 'M' | 90656 | 1956 |
8 | 'Michael' | 'M' | 90517 | 1958 |
19. 各行の1コラム目の文字列の出現頻度を求め,出現頻度の高い順に並べる
各行の1列目の文字列の出現頻度を求め,その高い順に並べて表示せよ.確認にはcut, uniq, sortコマンドを用いよ.
ここは素直に histcounts
関数を使う。ただ数値かカテゴリ型でないとだめな点に注意。
tabletxt = readtable('popular-names.txt','ReadVariableNames',false);
tabletxt.Properties.VariableNames = ["名前","性別","人数","年"];
tabletxt.("名前") = categorical(tabletxt.("名前"));
[freq, names] = histcounts(tabletxt.("名前"));
表示のためだけですがテーブル型に整形して、さらに頻度順に整列させます。
freqNames = table(names', freq', 'VariableNames', ["名前","出現頻度"]);
freqNames = sortrows(freqNames,'出現頻度','descend');
head(freqNames)
名前 | 出現頻度 | |
---|---|---|
1 | 'James' | 118 |
2 | 'William' | 111 |
3 | 'John' | 108 |
4 | 'Robert' | 108 |
5 | 'Mary' | 92 |
6 | 'Charles' | 75 |
7 | 'Michael' | 74 |
8 | 'Elizabeth' | 73 |
-
Livescript から markdown への変換は livescript2markdown: MATLAB's live scripts to markdown を使っています。 ↩