はじめに
もしかして 「え?MATLAB で言語処理やるの??」と思いました・・?
先日 言語処理 100 本ノック 2020 が公開されました。以前から気になってはいたんですが、これは言語処理という切り口で MATLAB の練習をするすごく良い題材じゃないか・・ということでまずは第 1 章の MATLAB コード例を公開します。
一緒にやってくれる MATLAB 芸人募集中!GitHub: NLP100-MATLAB
他章へのリンク
- 第 1 章: 準備運動(この記事)
- 第 2 章: UNIX コマンド
- 第 3 章: 正規表現
- 第 4 章: 形態素解析
- 第 6 章: 機械学習 part 1
- 第 6 章: 機械学習 part 2
- 第 7 章: 単語ベクトル
- 第 8 章: ニューラルネットワーク
環境
MATLAB R2020a(一部 Text Analytics Toolbox を使用しています)
この記事の Livescript 版(MATLAB)は GitHub: NLP100-MATLAB1 に置いてあります。同じ課題を実現する方法は沢山ある思います。もしもっといい(効率がいい、面白い)方法があれば是非コメントください。
00. 文字列の逆順
文字列”stressed”の文字を逆に(末尾から先頭に向かって)並べた文字列を得よ.
まずは string 型を使う場合。reverse
メソッドが使えそうです。
str = "stressed";
reverse(str)
ans = "desserts"
char 型を使った場合は、各文字を要素とする行列っぽく操作できます。
str = 'stressed';
disp(fliplr(str))
desserts
char 型の場合、インデックスを使って逆順に並べ替えることも可。
str(end:-1:1)
ans = 'desserts'
01. 「パタトクカシーー」
「パタトクカシーー」という文字列の1,3,5,7文字目を取り出して連結した文字列を得よ.
これは string 型より char 型の方が使いやすそう。
str = 'パタトクカシーー';
disp(str([1,3,5,7]))
パトカー
02. 「パトカー」+「タクシー」=「パタトクカシーー」
「パトカー」+「タクシー」の文字を先頭から交互に連結して文字列「パタトクカシーー」を得よ.
これも char 型の方がよさそう。同じ文字数なので、行列操作でやってみましょう。
str1 = 'パトカー';
str2 = 'タクシー';
str = [str1; str2]
str =
'パトカー'
'タクシー'
str(:)'
ans = 'パタトクカシーー'
1x4 と 1x4 の行列を連結させて、2x4 の行列を作った後に、コラムメジャーで並べ替えました。
03. 円周率
“Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics.”という文を単語に分解し,各単語の(アルファベットの)文字数を先頭から出現順に並べたリストを作成せよ.
ここも string 型でやってみます。スペースとコンマで分解します。
str = "Now I need a drink, alcoholic of course, after " + ...
"the heavy lectures involving quantum mechanics";
str = strsplit(str,{' ',','})'
str = 15x1 string
"Now"
"I"
"need"
"a"
"drink"
"alcoholic"
"of"
"course"
"after"
"the"
文字数は strlength メソッドですね。
numchars = strlength(str)'
numchars = 1x15
3 1 4 1 5 9 2 6 5 3 5 8 9 7 9
1x15 の数値ベクトルが出てきました。ちょっと見づらいので、文字列に変えて連結してみます。delimiter (区切り文字)に "" を指定することで間に空白が入らず文字連結。
join(string(numchars),"")
ans = "314159265358979"
04. 元素記号
“Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can.”という文を単語に分解し,1, 5, 6, 7, 8, 9, 15, 16, 19番目の単語は先頭の1文字,それ以外の単語は先頭に2文字を取り出し,取り出した文字列から単語の位置(先頭から何番目の単語か)への連想配列(辞書型もしくはマップ型)を作成せよ.
先頭の1文字、2文字を取り出すとなると char 型の方がやり易そう。strsplit
関数は同じように使えます。
str = ['Hi He Lied Because Boron Could Not Oxidize Fluorine ' ...
'New Nations Might Also Sign Peace Security Clause. Arthur King Can'];
str = strsplit(str)'
str = 20x1 cell
'Hi'
'He'
'Lied'
'Because'
'Boron'
'Could'
'Not'
'Oxidize'
'Fluorine'
'New'
分割しました。
先頭の1文字、2文字を取り出します。for ループで回してもいいですが、せっかくなので cellfun
を使います。
idx1 = [1,5,6,7,8,9,15,16]'; % 1 文字を取り出す index
idx2 = true(length(str),1); idx2(idx1) = false; idx2 = find(idx2); % 2 文字を取り出す index
% これは以下と同じ・・
% idx2 = [2,3,4,10,11,12,13,14,17,18,19,20];
matchStr1 = cellfun(@(x) x(1), str(idx1), "UniformOutput", false)
matchStr1 = 8x1 cell
'H'
'B'
'C'
'N'
'O'
'F'
'P'
'S'
matchStr2 = cellfun(@(x) x(1:2), str(idx2), "UniformOutput", false)
matchStr2 = 12x1 cell
'He'
'Li'
'Be'
'Ne'
'Na'
'Mi'
'Al'
'Si'
'Cl'
'Ar'
matchStr1
、matchStr2
ともにセル配列になっている点は要注意。
少しかっこつけて正規表現を使うなら・・
matchStr1 = regexp(str(idx1),'([a-zA-Z]{1}).*','tokens'); % 最初の1文字
matchStr2 = regexp(str(idx2),'([a-zA-Z]{2}).*','tokens'); % 最初の2文字
こんな感じ。
連想配列(辞書型もしくはマップ型)とのことなので、containers.Map を使ってみます。
keySet = [string(matchStr1); string(matchStr2)];
valueSet = [idx1; idx2];
M = containers.Map(keySet,valueSet);
M('Be')
ans =
4
Be は4番目。
table
型の方が見やすいかな?
t = table(valueSet,keySet);
sortrows(t,'valueSet') % 出現順にソート
valueSet | keySet | |
---|---|---|
1 | 1 | "H" |
2 | 2 | "He" |
3 | 3 | "Li" |
4 | 4 | "Be" |
5 | 5 | "B" |
6 | 6 | "C" |
7 | 7 | "N" |
8 | 8 | "O" |
9 | 9 | "F" |
10 | 10 | "Ne" |
11 | 11 | "Na" |
12 | 12 | "Mi" |
13 | 13 | "Al" |
14 | 14 | "Si" |
05. n-gram
与えられたシーケンス(文字列やリストなど)からn-gramを作る関数を作成せよ.この関数を用い,”I am an NLPer”という文から単語bi-gram,文字bi-gramを得よ.
単語 bi-gram とは文章内で連続する単語2つ、文字 bi-gram とは各単語内での連続する文字2つと理解します。Text Analytics Toolbox だと単語 n-gram はこんな感じ。
if license('checkout','Text_Analytics_Toolbox') % Toolbox が使える場合実行されます。
doc = tokenizedDocument("I am an NLPer"); % トークン化
bag = bagOfNgrams(doc,'NgramLengths',2);
bag.Ngrams
end
ans = 3x2 string
"I" "am"
"am" "an"
"an" "NLPer"
tokenizedDocument
を使わなくても単語単位で下と同じ処理をすればできますね。
次は文字 bi-gram 作ってみます。こんな関数を作ってみました。char ベクトルを入れたら、文字 n-gram を作って string 型配列で返す。文字数が少ないとそのまま返します。
function ngram = n_gram(word,NgramLength)
% word は char ベクトルを想定
N = length(word); % 文字の長さ
if N <= NgramLength
ngram = string(word);
else
ngram = strings(N-NgramLength,1);
for ii = 1:N-NgramLength+1
ngram(ii) = string(word(ii:ii+NgramLength-1));
end
ngram = unique(ngram); % 重複はいらない。
end
end
これを、cellfun
を使って適用してみると・・
str = "I am an NLPer";
str = split(str);
str = cellstr(str);
cellfun(@(x) n_gram(x,2), str, 'UniformOutput', false)
1 | |
---|---|
1 | 'I' |
2 | 'am' |
3 | 'an' |
4 | 4x1 string |
4 つ目だけ見難いですが、中身をみると
ans{4}
ans = 4x1 string
"LP"
"NL"
"Pe"
"er"
06. 集合
“paraparaparadise”と”paragraph”に含まれる文字bi-gramの集合を,それぞれ, XとYとして求め,XとYの和集合,積集合,差集合を求めよ.さらに,’se’というbi-gramがXおよびYに含まれるかどうかを調べよ.
X = n_gram('paraparaparadise',2)
X = 8x1 string
"ad"
"ap"
"ar"
"di"
"is"
"pa"
"ra"
"se"
Y = n_gram('paragraph',2)
Y = 7x1 string
"ag"
"ap"
"ar"
"gr"
"pa"
"ph"
"ra"
bi-gram 取れていますね。
集合演算は https://jp.mathworks.com/help/matlab/set-operations.html に詳細がありますが、和集合(union
)、積集合(intersect
)そして差集合(setdiff
)を使います。
union(X,Y)'
ans = 1x11 string
"ad" "ag" "ap" "ar" "di" "gr" "is" "pa" "ph" "ra" "se"
intersect(X,Y)'
ans = 1x4 string
"ap" "ar" "pa" "ra"
setdiff(X,Y)'
ans = 1x4 string
"ad" "di" "is" "se"
特定の文字が含まれているかどうかは contains
も使えそうですが、ここは単純に =
でやります。
どれか1つでも "se" と一致しますか? という論理変数を any
で求めます。
any(X == "se") | any(Y == "se")
ans =
1
'se' は存在しています。
07. テンプレートによる文生成
引数x, y, zを受け取り「x時のyはz」という文字列を返す関数を実装せよ.さらに,x=12, y=”気温”, z=22.4として,実行結果を確認せよ.
function sentence = fromTemplate(x,y,z)
sentence = x + "時の" + y + "は" + z;
end
引数のデータ型として何が使われるか多少不安ではありますが・・・
fromTemplate(12,"気温",22.4)
ans = "12時の気温は22.4"
08. 暗号文
与えられた文字列の各文字を,以下の仕様で変換する関数cipherを実装せよ.
- 英小文字ならば(219 - 文字コード)の文字に置換
- その他の文字はそのまま出力
この関数を用い,英語のメッセージを暗号化・復号化せよ.
まずは char 型から始めてみます。
isstrprop
で小文字がどこにあるかを調べられるので使ってみます。
str = 'We are the borg. Resistance is futile.';
idxLower = isstrprop(str,'lower');
小文字だけ (219 - 文字コード) に対応する文字に変換します。string
型に使える文字を置き換える replace
を試します。
まずは置換する文字を見つけてきて、double
に与えると・・文字コードになりますので、219 - 文字コードを計算して文字に戻してみます。
unique(str(idxLower))
ans = 'abcefghilnorstu'
219-double(ans)
ans = 1x15
122 121 120 118 117 116 115 114 111 109 108 105 104 103 102
char
に与えると元に戻ります。
char(ans)
ans = 'zyxvutsromlihgf'
上の方法を使って置換してみると、こんな感じ。
str = 'We are the borg. Resistance is futile.';
idxLower = isstrprop(str,'lower');
str(idxLower) = char(219-double(str(idxLower)))
str = 'Wv ziv gsv ylit. Rvhrhgzmxv rh ufgrov.'
同じ処理で元に戻りますので、これを cipher
として実装すればOK.
idxLower = isstrprop(str,'lower');
str(idxLower) = char(219-double(str(idxLower)))
str = 'We are the borg. Resistance is futile.'
09. Typoglycemia
スペースで区切られた単語列に対して,各単語の先頭と末尾の文字は残し,それ以外の文字の順序をランダムに並び替えるプログラムを作成せよ.ただし,長さが4以下の単語は並び替えないこととする.適当な英語の文(例えば”I couldn’t believe that I could actually understand what I was reading : the phenomenal power of the human mind .”)を与え,その実行結果を確認せよ.
char
型の方がやり易いかな。
sentence = ['I couldn''t believe that I could actually understand ' ...
'what I was reading : the phenomenal power of the human mind.'];
words = strsplit(sentence)' % 各単語が1つずつセルに入ります。
words = 20x1 cell
'I'
'couldn't'
'believe'
'that'
'I'
'could'
'actually'
'understand'
'what'
'I'
それぞれの単語について、5文字以上であれば間の文字をランダムに入れかえる処理をします。
こんな関数を作ってみました。各単語の入力は char
型とします。
function word2 = randomizeWord(word1)
if length(word1) <= 4
word2 = word1;
else
word2 = word1;
tmp = word2(2:end-1);
idx = randperm(length(tmp)); % tmp の文字数
word2(2:end-1) = tmp(idx);
end
end
randperm
については tmp
の文字数を N とすると、1 から N までの数字をランダムに入れ替えた数列を返します。その数値をインデックスとすれば入れ替え可能!
ここでは cellfun
を使ってセル内にある各単語にそれぞれ適用します。
tmp = cellfun(@randomizeWord, words, 'UniformOutput', false);
join(tmp) % 単語をつなぐ。
ans =
{'I coldun't bevelie that I colud alcaulty usntarnded what I was reading : the paeoenhnml power of the huamn mndi.'}
できました。
Appendix: 関数
05. n-gram
function ngram = n_gram(word,NgramLength)
% word は char ベクトルを想定
N = length(word); % 文字の長さ
if N <= NgramLength
ngram = word;
else
ngram = strings(N-NgramLength,1);
for ii = 1:N-NgramLength+1
ngram(ii) = word(ii:ii+NgramLength-1);
end
ngram = unique(ngram); % 重複はいらない。
end
end
07. テンプレートによる文生成
function sentence = fromTemplate(x,y,z)
sentence = x + "時の" + y + "は" + z;
end
09. Typoglycemia
function word2 = randomizeWord(word1)
if length(word1) <= 4
word2 = word1;
else
word2 = word1;
tmp = word2(2:end-1);
idx = randperm(length(tmp));
word2(2:end-1) = tmp(idx);
end
end
-
Livescript から markdown への変換は livescript2markdown: MATLAB's live scripts to markdown を使っています。 ↩