写在前面
总结一下最近学习 T i k Z \mathrm{Ti}k\mathrm{Z} TikZ时候的一些心得,以及使用 T i k Z \mathrm{Ti}k\mathrm{Z} TikZ绘制LSTM元胞结构的一个小例子。在之前的文章中,我曾经尝试过使用 T i k Z \mathrm{Ti}k\mathrm{Z} TikZ绘制组合数学中的棋盘格(有兴趣的话请看《LaTeX TikZ绘图包——组合数学中棋盘格的画法》),经过一周多的学习(啃文档),终于大概理解了 T i k Z \mathrm{Ti}k\mathrm{Z} TikZ的精妙,在一篇stackflow的问答中(How do I draw an LSTM cell in Tikz?)
Windows 10 x64
MiKTeX 20.10+TeXworks
TikZ是什么?
T i k Z \mathrm{Ti}k\mathrm{Z} TikZ( T i k Z \mathrm{Ti}k\mathrm{Z} TikZ ist kein Zeichenprogramm)是以PGF(portable graphic format)为后端的一个绘图前端,用于绘制矢量图(几何,图论等),由Till Tantau教授等开发,以 TeX \TeX TEX为引擎,在矢量图绘图方面十分强大,可惜学习曲线比较陡峭,而且目前没有相关的中文书籍(下面的官方文档中译版内容不全),所以只能多看多练,当然也可以借助 T i k Z E d t \mathrm{Ti}k\mathrm{ZEdt} TikZEdt编辑器可视化生成图形。
学习的心得体会
从一开始逐字逐句看文档,到后来结合文档中的具体例子一点一点实现,感觉自己对其绘图的语句有一点点理解了, T i k Z \mathrm{Ti}k\mathrm{Z} TikZ绘图的主要结构其实理解成web前端,内容(html)与样式(CSS)分离,这样可以方便地扩展图形,再到后来需要一些装饰,都可以在 T i k Z s e t \mathrm{Ti}k\mathrm{Zset} TikZset中实现,之后如果要再添加一些功能,就需要一些更高级的扩展包来实现了,这里类比一下JS应该不过分😆.
总之,引用stack exchange的一句话, T i k Z \mathrm{Ti}k\mathrm{Z} TikZ真的会让人上瘾!看那些精美的图形,是否你也跃跃欲试了呢?
LSTM元胞结构图的绘制
以下是一个 T i k Z \mathrm{Ti}k\mathrm{Z} TikZ绘图的小例子,参考了stackexchange网站的代码,加上了一些自己喜欢的配置(配色丑,见谅……),总体来说在代码实现上不难。
%cite by https://tex.stackexchange.com/questions/432312/how-do-i-draw-an-lstm-cell-in-tikz
\documentclass[tikz,border=10pt]{standalone}
% 使用宏包:bm用于加粗数学符号,ctex用于中文支持,xcolor用于配色
\usepackage{bm,ctex,xcolor}
% 使用TikZ内部的扩展包:calc计算两点间等分点,arrows.meta用于定制箭头样式,shapes用于椭圆的生成
\usetikzlibrary{calc, positioning, arrows.meta, shapes}
% 定义新命令 \vars{var1}{var2}用于符号的定制
\newcommand{\vars}[2]{${\bm#1}_{\bm#2}$}
\begin{document}
\begin{tikzpicture}[
% 定义样式
myfont/.style={
font=\footnotesize\sffamily
},
frame/.style={% 外部边框(圆角矩形)的样式
rectangle,
rounded corners=5mm,
draw,gray,
fill=lime!45,
very thick,
},
add/.style={% 加操作符的样式
circle,
draw, thick,
fill=yellow!60,
inner sep=.5pt,
minimum height =3mm,
},
prod/.style={% 乘操作符的样式
circle,
draw, thick,
fill=lightgray!40!blue!30,
inner sep=.5pt,
minimum height =3mm,
},
function/.style={% 信息处理函数(tanh)的样式
ellipse,
draw, thick,
fill=cyan!30,
inner sep=1pt
},
cell/.style={%C_t和C_{t-1}的样式
circle,draw,fill=magenta!30,
line width = .75pt,
minimum width=8mm,
inner sep=1pt,
},
hidden/.style={% h_t和h_{t-1}的样式
circle,draw,fill=orange!50,
line width = .75pt,
minimum width=8mm,
inner sep=1pt,
},
input/.style={% x_t的样式
circle,draw,
fill=teal!30,
line width = .75pt,
minimum width=8mm,
inner sep=1pt,
},
actfunc/.style={% 激活函数的样式
rectangle, draw,
fill=pink!30,
minimum width=4.25mm,
minimum height=3.75mm,
inner sep=1pt,
thick,
},
ArrowC1/.style={% 线段的圆角样式
rounded corners=2mm,
>=Stealth[round],
very thick,
},
ArrowC2/.style={% 弧度略大的圆角样式
rounded corners=3mm,
>=Stealth[round],
very thick,
},
]
% 图形绘制部分
% 绘制外部圆角边框
\node [frame, minimum height =4cm, minimum width=6cm] at (0,0) {};
% 绘制激活函数结点
\node [actfunc] (ibox1) at (-2,-0.8) {$\bm\sigma$};
\node [actfunc] (ibox2) at (-1.35,-0.8) {$\bm\sigma$};
\node [actfunc, minimum width=9mm] (ibox3) at (-0.45,-0.8) {$\bm\tanh$};
\node [actfunc] (ibox4) at (0.5,-0.8) {$\bm\sigma$};
% 绘制操作符结点
\node [prod] (mux1) at (-2,1.5) {$\bm\times$};
\node [add] (add1) at (-0.5,1.5) {$\bm+$};
\node [prod] (mux2) at (-0.5,0) {$\bm\times$};
\node [prod] (mux3) at (1.5,0) {$\bm\times$};
\node [function] (func1) at (1.5,0.75) {$\bm\tanh$};
% 绘制神经元外部输入节点
\node[cell, label={[myfont]above:长时记忆输入}] (c) at (-4,1.5) {\vars{c}{t-1}};
\node[hidden, label={[myfont]above:隐层输入}] (h) at (-4,-1.5) {\vars{h}{t-1}};
\node[input, label={[myfont]right:输入}] (x) at (-2.5,-3) {\vars{x}{t}};
% 绘制神经元外部输出节点
\node[cell, label={[myfont]above:长时记忆输出}] (c2) at (4,1.5) {\vars{c}{t}};
\node[hidden, label={[myfont]below:短时记忆输出}] (h2) at (4,-1.5) {\vars{h}{t}};
\node[hidden, label={[myfont]left:输出}] (x2) at (2.5,3) {\vars{h}{t}};
% 绘制各节点之间的连接线
% 使用相交和位移
% 绘制C_{t-1}到C_t的线
\draw [->, ArrowC1] (c) -- (mux1) -- (add1) -- (c2);
% 绘制输入线段
\draw [ArrowC2] (h) -| (ibox4);
\draw [ArrowC1] (h) -| (ibox1);
\draw [ArrowC1] (h) -| (ibox2);
\draw [ArrowC1] (h) -| (ibox3);
\draw [ArrowC1] (x) -- (x |- h) -| (ibox1);
% 内部线段(带箭头)
\draw [->, ArrowC2] (ibox1) -- (mux1);
\node [label=left:$\bm{f_t}$,right=1.5pt] () at ($(ibox1)!.5!(mux1)$) {};
\draw [->, ArrowC2] (ibox2) |- (mux2);
\node [label=left:$\bm{i_t}$, right=2pt] () at (ibox2 |- mux2) {};
\draw [->, ArrowC2] (ibox3) -- (mux2);
\node [label=right:$\bm{\tilde{C}_t}$, left=.5pt] () at ($(ibox3)!.5!(mux2)$) {};
\draw [->, ArrowC2] (ibox4) |- (mux3);
\node [label=left:$\bm{o_t}$, right=2pt] () at (ibox4 |- mux3) {};
\draw [->, ArrowC2] (mux2) -- (add1);
\draw [->, ArrowC1] (add1) -| (func1);
\draw [->, ArrowC2] (func1) -- (mux3);
%输出线段
\draw [->, ArrowC2] (mux3) |- (h2);
%标记出现缺口线段的下端点
\draw (c2 -| x2) ++(0,-0.15) coordinate (i1);
\draw [-, ArrowC2] (h2 -| x2) -| (i1);
\draw [->, ArrowC2] (i1)++(0,0.3) -- (x2);
\end{tikzpicture}
\end{document}
编译结果: