CCG導出木作図で構成素の位置を最適化で解く
中谷瞳『ことばの意味を計算するしくみ」(講談社、2024)を読んでいて、第8章でCCG導出木というものが登場した。論理学のシーケント図で話を展開してきた流れで、CCGの説明をしているので、シーケント図の変種といった位置づけだろうが、統語木構造に関数適用の規則をくっつけているような図で、割と横に広がった木構造の図となる。

そこで、Texで描画するにはどうすればうまく描けるだろうか、とちょっと気になった。Texで描くというのは、Tikz(Tikzpicture)を利用しての描画のことである。Texで木構造を描くのはそんなに難しい印象はない。テキストをノードとして枠線をつけて、ノード間に線を引けばよい。
CCG導出木は、シーケント図もそうだが、そのような線は無く、ノードとなるテキストの上下に横線が引かれる、といった具合だ。これは、上下のテキストの幅を比較して線を引けばよいだろう。そうはいっても、だいたいのシーケント図に現れるの程度の論理式なら、そんなに長くないので、目算で適当に線を引っ張れるが、CCG導出木のノードのテキスト、つまり統語範疇:意味表示(ラムダ式)はかなり長い。この段階で、方眼紙を出してきて、下書きする必要が出てくる。
さらに、横方法(x方向)をみると、係わりのない親ノードについて領域的に(xの範囲で)重複がないのだ。もちろん、この木構造において、異なる親の間であっても、子供の配置の順序が変わらなければ、エッジが交差することはない。とはいえ、親子関係を横線で表す場合は、親から離れないほうがよい。つまり図としては、親で横方向の範囲が決まり、ある親の子供は、その親の横方向の範囲に収まっているのが見やすい。子供が親となるときには、その子供は親の範囲に配置され、兄弟や親の違うノードとは範囲を跨がなくする。これは、親の横方向の範囲は、一番左の子供の左端と一番右の子供の右端となる、ということである。また親の配置は子供の配置の中心にあるとよい。
なお、縦方向(y方向)は1行ずつ並んでいる。
親と子の位置関係は、具体的なノードのテキストに関わらずに決まる関係であって、よって制約条件(拘束条件)として表現できるだろう。よって最適化で位置を求めてやれば、わざわざ方眼紙で手書きで確認せずに描けるだろう。
Pythonを使い、最適化はpulpを利用する。
まずは、ノードの情報をクラスとする。
import pulp
sp_min = 0.3 # 同じ列にあるノードの横方向の最小間隔
step_len = 0.6 # 縦方向の段の間隔(行間隔)
class ELM:
def __init__(self, nm, l=0):
self.name = nm
self.x = pulp.LpVariable(nm+'_x')
self.y = pulp.LpVariable(nm+'_y')
self.l = l
def SetName(self, nm):
self.name = nm
def SetText(self, txt):
self.text = txt
def SetX(self, x): # constraint Variable
self.x = x
def SetY(self, y): # constraint variable
self.y = y
def SetL(self, l):
self.l = l
def GetName(self):
return self.name
def GetText(self):
return self.text
def GetX(self): # constraint Variable
return self.x
def GetY(self): # constraint variable
return self.y
def GetL(self):
return self.l
def GetCenterString(self):
tmpx = self.x.value() + self.l*0.5
tmpy = self.y.value()
return '('+str(round(tmpx,1))+','+str(round(tmpy,1))+')'
def GetLeftXValue(self):
return self.x.value()
def GetRightXValue(self):
return self.x.value() + self.l
def GetYValue(self):
return self.y.value()X(横方向)とY(縦方向)の座標を指定する制約である。基準となる要素の位置を決める。
def XPos(prob, elm, x):
prob += elm.GetX()==x
def YPos(prob, elm, y):
prob += elm.GetY()==y
同列にあるノードは、最小間隔以上、離れている。
def ELM1LeftELM2(prob, e1, e2):
prob += e1.GetX()+e1.GetL()+sp_min<=e2.GetX()親子関係の、横方向の位置の制約条件である。type1は子ノードが一つの場合に中心を揃えること、type2は、親ノードは、左端と右端の子ノードの中心にあること、である。
def InterStepCenterConstraintType1(prob, e1, e2):
prob += e1.GetX()+0.5*e1.GetL()==e2.GetX()+0.5*e2.GetL()
def InterStepCenterConstraintType2(prob, e1, e2, e3):
prob += e1.GetX()+0.5*e1.GetL()==0.5*(e2.GetX()+e3.GetX()+e3.GetL())縦方向の、行間の制約である。半分の行間の位置は線と関数適用規則(「<」や「>」といった記号」)の高さ位置を決めるためのものである。
def ELM1StepBelowELM2(prob, e1, e2):
prob += e1.GetY()+step_len==e2.GetY()
def ELM1HalfStepBelowELM2(prob, e1, e2):
prob += e1.GetY()+0.5*step_len==e2.GetY()
def ELM1SameStepELM2(prob, e1, e2):
prob += e1.GetY()==e2.GetY()以上が、CCG導出木の構成素の配置の制約条件の設定である。
先に示した図を描画するための例について示す。
目的は、全ての構成素のx座標値の総和の最小化、とする。
elm1_1 = ELM('1_1', l=4)
elm1_1.SetText('$S:\\mathsf{love(john,mary)}$')
elm2_1 = ELM('2_1', l=2)
elm2_1.SetText('$NP:\\mathsf{john}$')
elm2_2 = ELM('2_2', l=4)
elm2_2.SetText('$S\\backslash NP:\\lambda x.\\mathsf{love}(x,\\mathsf{mary})$')
add1_2_1 = ELM('1_2_1', l=1)
add1_2_1.SetText('$<$')
elm3_1 = ELM('3_1', l=2)
elm3_1.SetText('John')
elm3_2 = ELM('3_2', l=5)
elm3_2.SetText('$(S\\backslash NP)/NP:\\lambda x.\\mathsf{love}(x,y)$')
elm3_3 = ELM('3_3', l=2)
elm3_3.SetText('$NP:\\mathsf{mary}$')
add2_3_1 = ELM('2_3_1', l=1)
add2_3_1.SetText('$>$')
elm4_1 = ELM('4_1', l=2)
elm4_1.SetText('loves')
elm4_2 = ELM('4_2', l=2)
elm4_2.SetText('Mary')
prob = pulp.LpProblem('CCG_derivation_tree_diagram', sense=pulp.LpMinimize)
# reference position (elm2_1_x and elm1_1_y)
XPos(prob, elm3_1, 0)
YPos(prob, elm1_1, 0)
# element same row x position
ELM1LeftELM2(prob, elm2_1, elm2_2)
ELM1LeftELM2(prob, elm2_2, add1_2_1)
ELM1LeftELM2(prob, elm3_1, elm3_2)
ELM1LeftELM2(prob, elm3_2, elm3_3)
ELM1LeftELM2(prob, elm3_3, add2_3_1)
ELM1LeftELM2(prob, elm4_1, elm4_2)
# inter-step horizontal constraints (element center x position, etc.)
InterStepCenterConstraintType2(prob, elm1_1, elm2_1, elm2_2)
InterStepCenterConstraintType1(prob, elm2_1, elm3_1)
InterStepCenterConstraintType2(prob, elm2_2, elm3_2, elm3_3)
InterStepCenterConstraintType1(prob, elm3_2, elm4_1)
InterStepCenterConstraintType1(prob, elm3_3, elm4_2)
# element y position
ELM1StepBelowELM2(prob, elm1_1, elm2_1)
ELM1SameStepELM2(prob, elm2_1, elm2_2)
ELM1StepBelowELM2(prob, elm2_1, elm3_1)
ELM1SameStepELM2(prob, elm3_1, elm3_2)
ELM1SameStepELM2(prob, elm3_1, elm3_3)
ELM1StepBelowELM2(prob, elm3_1, elm4_1)
ELM1SameStepELM2(prob, elm4_1, elm4_2)
ELM1HalfStepBelowELM2(prob, elm1_1, add1_2_1)
ELM1HalfStepBelowELM2(prob, elm2_1, add2_3_1)
# objective
lis = [elm1_1, elm2_1, elm2_2, add1_2_1, elm3_1, elm3_2, elm3_3, add2_3_1, elm4_1, elm4_2]
def SetObjective(prob, lis):
o = lis[0].GetX()
for i in range(1, len(lis)):
o += lis[i].GetX()
prob += o
SetObjective(prob, lis)
print(prob)
status = prob.solve()
print('Status', pulp.LpStatus[status])
print(prob)
status = prob.solve()
print('Status', pulp.LpStatus[status])
print('Result')
for v in prob.variables():
print(v.name, ':', v.value())
print('z', prob.objective.value())
p_s1_1=elm1_1.GetCenterString()
p_s1_2_1=add1_2_1.GetCenterString()
p_s2_1=elm2_1.GetCenterString()
p_s2_2=elm2_2.GetCenterString()
p_s2_3_1=add2_3_1.GetCenterString()
p_s3_1=elm3_1.GetCenterString()
p_s3_2=elm3_2.GetCenterString()
p_s3_3=elm3_3.GetCenterString()
p_s4_1=elm4_1.GetCenterString()
p_s4_2=elm4_2.GetCenterString()
def GetLeftString(e1, e2):
x = e1.GetLeftXValue() if e1.GetLeftXValue() <= e2.GetLeftXValue() else e2.GetLeftXValue()
y = e1.GetYValue()+0.5*step_len
return '('+str(round(x,1))+','+str(round(y,1))+')'
def GetRightString(e1, e2):
x = e1.GetRightXValue() if e1.GetRightXValue() >= e2.GetRightXValue() else e2.GetRightXValue()
y = e1.GetYValue()+0.5*step_len
return '('+str(round(x,1))+','+str(round(y,1))+')'
ln1_2_1_sts=GetLeftString(elm1_1, elm2_1)
ln1_2_1_ens=GetRightString(elm1_1, elm2_2)
ln2_3_1_sts=GetLeftString(elm2_1, elm3_1)
ln2_3_1_ens=GetRightString(elm2_1, elm3_1)
ln2_3_2_sts=GetLeftString(elm2_2, elm3_2)
ln2_3_2_ens=GetRightString(elm2_2, elm3_3)
ln3_4_1_sts=GetLeftString(elm3_2, elm4_1)
ln3_4_1_ens=GetRightString(elm3_2, elm4_1)
ln3_4_2_sts=GetLeftString(elm3_3, elm4_2)
ln3_4_2_ens=GetRightString(elm3_3, elm4_2)
ln1_2_1_s='\\draw '+ln1_2_1_sts+' to '+ln1_2_1_ens+';'
ln2_3_1_s='\\draw '+ln2_3_1_sts+' to '+ln2_3_1_ens+';'
ln2_3_2_s='\\draw '+ln2_3_2_sts+' to '+ln2_3_2_ens+';'
ln3_4_1_s='\\draw '+ln3_4_1_sts+' to '+ln3_4_1_ens+';'
ln3_4_2_s='\\draw '+ln3_4_2_sts+' to '+ln3_4_2_ens+';'
s1_1 = '\\node (s1_1) at ' + p_s1_1 + ' {' + elm1_1.GetText() + '};'
s1_2_1 = '\\node (s1_2_1) at ' + p_s1_2_1 + ' {' + add1_2_1.GetText() + '};'
s2_1 = '\\node (s2_1) at ' + p_s2_1 + ' {' + elm2_1.GetText() + '};'
s2_2 = '\\node (s2_2) at ' + p_s2_2 + ' {' + elm2_2.GetText() + '};'
s2_3_1 = '\\node (s2_3_1) at ' + p_s2_3_1 + ' {' + add2_3_1.GetText() + '};'
s3_1 = '\\node (s3_1) at ' + p_s3_1 + ' {' + elm3_1.GetText() + '};'
s3_2 = '\\node (s3_2) at ' + p_s3_2 + ' {' + elm3_2.GetText() + '};'
s3_3 = '\\node (s3_3) at ' + p_s3_3 + ' {' + elm3_3.GetText() + '};'
s4_1 = '\\node (s4_1) at ' + p_s4_1 + ' {' + elm4_1.GetText() + '};'
s4_2 = '\\node (s4_2) at ' + p_s4_2 + ' {' + elm4_2.GetText() + '};'ln1_2_1_sやs1_1を出力すれば、Texのソースを得られる。
\node (s1_1) at (4.0,0.0) {$S:\SF{love(john,mary)}$};
\node (s1_2_1) at (8.8,0.3) {$<$};
\node (s2_1) at (1.0,0.6) {$NP:\mathsf{john}$};
\node (s2_2) at (6.0,0.6) {$S\backslash NP:\lambda x.\mathsf{love}(x,\mathsf{mary})$};
\node (s2_3_1) at (10.4,0.9) {$>$};
\node (s3_1) at (1.0,1.2) {John};
\node (s3_2) at (4.8,1.2) {$(S\backslash NP)/NP:\lambda x.\mathsf{love}(x,y)$};
\node (s3_3) at (8.6,1.2) {$NP:\mathsf{mary}$};
\node (s4_1) at (4.8,1.8) {loves};
\node (s4_2) at (8.6,1.8) {Mary};
\draw (0.0,0.3) to (8.0,0.3);
\draw (0.0,0.9) to (2.0,0.9);
\draw (2.3,0.9) to (9.6,0.9);
\draw (2.3,1.5) to (7.3,1.5);
\draw (7.6,1.5) to (9.6,1.5);
いいなと思ったら応援しよう!
応援お願いします。チップは元気の源です。
