カルチャーとデータサイエンス

ビジネスの意思決定は無理でも、せめてカルチャーはデータサイエンスしたい

言語処理100本ノックをひたすら解く ~ No.00 ~ 09(準備運動)

はじめに

pira-nino.hatenablog.com

上記記事みたいなことをやりたいのですが、そもそも自然言語の前処理にそれほど明るくないということで・・まずは修行したいと思います。

当記事の目的

言語処理100本ノック 2015の問題00 ~ 09を実装します。いろいろ忘れているPythonの文法等もまとめて復習します。

参考元

回答の参考として、主に以下Qiitaの記事を参照しました。

qiita.com

準備

実装は全てPythonで行います。

方針としては、pyファイルではなくjupyter notebook上で、関数やクラス形式で統一して実装することとします。

実装

00. 文字列の逆順

(問)文字列"stressed"の文字を逆に(末尾から先頭に向かって)並べた文字列を得よ.

(解)

def nlp000(s: str):
    return s[::-1]

nlp000("stressed") 
# 'desserts'

(注釈)

  • リストのスライスを使用します。[i:j:n]でi文字目からj文字目をn個ごとに取得できます。
  • i, jを指定しなければ全ての文字を取得します。
  • nを負の値で指定すれば、逆順に文字列を取得できます。-1で1つずつ逆順で取得します。

01. 「パタトクカシーー」

(問)「パタトクカシーー」という文字列の1,3,5,7文字目を取り出して連結した文字列を得よ.

(解)

def nlp001(s: str) -> str:
    return s[::2]

nlp001("パタトクカシーー")

(注釈)

  • 01と同じくスライスを使用します。

02. 「パトカー」+「タクシー」=「パタトクカシーー」

(問)「パトカー」+「タクシー」の文字を先頭から交互に連結して文字列「パタトクカシーー」を得よ.

(解)

def nlp002(s1: str, s2: str) -> str:
    result = "".join([i + j for i, j in zip("パトカー", "タクシー")])
    return result

nlp002("パトカー", "タクシー")

(注釈)

  • zip(): 複数のイテラブルなオブジェクト(文字列、リストなど)をまとめる際に使用します。for文のループ等に便利です。

03. 円周率

(問) "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."という文を単語に分解し,各単語の(アルファベットの)文字数を先頭から出現順に並べたリストを作成せよ

(解)

# 自分の答え
def nlp003(s: str) -> str:
    list = s.split()
    result = [len(i) for i in list]
    return result

nlp003("Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics.")
# [3, 1, 4, 1, 6, 9, 2, 7, 5, 3, 5, 8, 9, 7, 10]


# 模範解答
def nlp003a(s: str) -> str:
    import re
    return list(len(m) for m in re.findall('\w+', s))

nlp003a("Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics.")
# [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9]

(注釈)

  • リスト内包表記で書きます。この場合の構文は[式 for 変数 in イテラブル]となります。
  • 内包表記はコードがすっきりするし、処理も速いそうなので、積極的に使用します。(参考→Pythonのリスト内包表記の書き方 | HEADBOOST
  • 自分の答えと模範解答(「参照」項より」)を記載しています。自分の答えでは記号(ピリオドなど)の処理が上手くできていないのが分かります。模範解答では正規表現を用いていますね。正規表現は今後の問題にも含まれるので、そのときに注釈を加えます。

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文字を取り出し,取り出した文字列から単語の位置(先頭から何番目の単語か)への連想配列(辞書型もしくはマップ型)を作成せよ.

(解)

def nlp004(s: str) -> str:
    list = s.split()
    first_char = [1, 5, 6, 7, 8, 9, 15, 16, 19]
    result = {}
    for i, word in enumerate(list, 1):
        if i in first_char:
            result[word[0]] = i
        else:
            result[word[0:2]] = i
    return result
    
nlp004("Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can.")
# {'H': 1, 'He': 2, 'Li': 3, 'Be': 4, 'B': 5, 'C': 6, 'N': 7, 'O': 8, 'F': 9, 'Ne': 10, 'Na': 11, 'Mi': 12, 'Al': 13, 'Si': 14, 'P': 15, 'S': 16, 'Cl': 17, 'Ar': 18, 'K': 19, 'Ca': 20}

(注釈)

  • 水兵リーベは海外だとこんな覚え方するんですね。
  • 辞書型(マップ型)は、{key: value}の組み合わせで要素を格納するオブジェクトです。pythonだと辞書型という呼び名が一般的で、連想配列とかmapという言い方は他の言語でよく用いられているような印象です。
  • enumerate(リテラブル, n)は、for文で要素とそのインデックス番号を同時に取得したい場合に用います。nに任意の数値を指定することで、インデックスの開始位置を指定できます。通常のリストは最初の要素が0番目となるので、ここでは1番目に振り直すよう定義しています。

05. n-gram

(問)与えられたシーケンス(文字列やリストなど)からn-gramを作る関数を作成せよ.この関数を用い,"I am an NLPer"という文から単語bi-gram,文字bi-gramを得よ.

(解)

# 自分の答え
def nlp005(s: str, n: int) -> str:
    result = [s[idx:idx + n] for idx in range(len(s) - n + 1)]
    return result

#文字n-gram
print(nlp005("I am an NLPer", 1))
print(nlp005("I am an NLPer", 2))

# ['I', ' ', 'a', 'm', ' ', 'a', 'n', ' ', 'N', 'L', 'P', 'e', 'r']
# ['I ', ' a', 'am', 'm ', ' a', 'an', 'n ', ' N', 'NL', 'LP', 'Pe', 'er']

#単語n-gram
words = "I am an NLPer".split(' ')
print(nlp005(words, 1))
print(nlp005(words, 2))

# [['I'], ['am'], ['an'], ['NLPer']]
# [['I', 'am'], ['am', 'an'], ['an', 'NLPer']]

(注釈)

  • n-gram: 任意の文字列や文書を連続したn個の文字で分割するテキスト分割方法のことです。特に、nが1の場合をユニグラム(uni-gram)、2の場合をバイグラム(bi-gram)、3の場合をトライグラム(tri-gram)と呼びます。(参考→Nグラム(えぬぐらむ)とは - コトバンク
  • 文字n-gramは文字単位、単語n-gramは単語単位でテキストを分割します。英語の場合は、単語単位でのテキスト分割は半角スペースで分割します。

06. 集合

(問) "paraparaparadise"と"paragraph"に含まれる文字bi-gramの集合を,それぞれ, XとYとして求め,XとYの和集合,積集合,差集合を求めよ.さらに,'se'というbi-gramがXおよびYに含まれるかどうかを調べよ.

(解)

class Nlp006:
    def __init__(self, n):
        self.n = n
        self.s1_gram_ = None
        self.s2_gram_ = None
    
    def fit(self, s1: str, s2: str) -> str:
        self.s1_gram_ = {s1[idx:idx + self.n] for idx in range(len(s1) - self.n + 1)}
        self.s2_gram_ = {s2[idx:idx + self.n] for idx in range(len(s2) - self.n + 1)}
        
    def predict(self, type):
        if type == "union":
            return self.s1_gram_ | self.s2_gram_
        elif type == "intersection":
            return self.s1_gram_ & self.s2_gram_
        elif type == "difference":
            return self.s1_gram_ - self.s2_gram_

nlp006 = Nlp006(n=2)
nlp006.fit("paraparaparadise", "paragraph")
print(nlp006.predict(type="union"))
# {'ad', 'ra', 'gr', 'ap', 'pa', 'ph', 'is', 'ar', 'ag', 'di', 'se'}

print(nlp006.predict(type="intersection"))
# {'pa', 'ap', 'ar', 'ra'}

print(nlp006.predict(type="difference"))
# {'ad', 'is', 'se', 'di'}

(注釈)

  • 機械学習チックに、fit関数とpredict関数を持つクラスを作成しました。
  • 集合型(セット型)は辞書型と同じく{}で定義されるオブジェクトですが、重複要素を持たない、順番を持たないという特徴があります。そのため和集合や積集合の演算に便利です。

07. テンプレートによる文生成

(問)引数x, y, zを受け取り「x時のyはz」という文字列を返す関数を実装せよ.さらに,x=12, y="気温", z=22.4として,実行結果を確認せよ.

(解)

def nlp007(x:str, y:str, z:str) -> str:
    return "{}時の{}は{}".format(x, y, z)

nlp007(x=12, y="気温", z=22.4)
# '12時の気温は22.4'

(注釈)

  • format():文字列変換に使用します。文字列型(str)が持つメソッドとしても機能します。{}は置換フィールドと呼ばれ、format()で指定する引数に置き換えられます。

08. 暗号文

(問)与えられた文字列の各文字を,以下の仕様で変換する関数cipherを実装せよ.

  • 英小文字ならば(219 - 文字コード)の文字に置換
  • その他の文字はそのまま出力

この関数を用い,英語のメッセージを暗号化・復号化せよ.

(解)

def nlp008(s: str) -> str:
    result = "".join([chr(219 - ord(i)) if "a" <= i <= "z" else i for i in s ])
    return result

print(nlp008("The rain in Spain stays mainly in the plain."))
# 暗号: 'Tsv izrm rm Skzrm hgzbh nzrmob rm gsv kozrm.'

print(nlp008(nlp008("The rain in Spain stays mainly in the plain.")))
# 復号: The rain in Spain stays mainly in the plain.

(注釈)

  • ord(): 文字を文字コードに変換する関数です。
  • chr(): 文字コードを文字に変換する関数です。
  • この暗号化方法は、同じ暗号化をもう一度繰り返すことで復号が可能です。

09. Typoglycemia

(問) スペースで区切られた単語列に対して,各単語の先頭と末尾の文字は残し,それ以外の文字の順序をランダムに並び替えるプログラムを作成せよ.ただし,長さが4以下の単語は並び替えないこととする.適当な英語の文(例えば"I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind .")を与え,その実行結果を確認せよ.

(解)

def nlp009(s: str) -> str:
    from random import sample
    list = s.split()
    result_list = []
    for i in list:
        if len(i) <= 4:
            result_list.append(i)
        else:
            result_list.append(i[0] + "".join(sample(i[1:-1], len(i) - 2)) + i[-1])
    result = " ".join(result_list)
    return result

nlp009("I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind .")
# 例: "I cdol'unt bliveee that I cuold atllcuay unestradnd what I was riadneg : the poanehmnel pewor of the human mind ."

(注釈)

  • 「この ぶんょしう は いりぎす の ケブンッリジ だがいく の けゅきんう の けっか... 」という有名な日本語のネタがありますね。
  • random.sample(): ランダムに複数の要素を重複なしで取得します。第一引数にリスト、第二引数に取得したい要素の数を指定します。

Todo

  • 言うまでもなく続き(No.10~以降)
  • 内包表記、文字コードの扱いはどこかで復習する

おわりに

もともと歌詞分析とか音楽周りのことを書くブログの予定だったけどネタがない・・ 気が向いたら更新します。