格式化
>格式化標記通常只能是產生幾種不同文本的樣式,但在語義上它們各自有著自己的特點
>如果你只是想有著一些自己的文本樣式,可以嘗試使用“常用html標記”里格式化的內容或css樣式表
>但是這樣會對搜索引擎不友好
>所以我們強調使用語義化標簽,提供給瀏覽器的語義信息越多,瀏覽器就可以越好地把這些信息展示給用戶。
## `<abbr></abbr>`
作用:用于指示該標簽內的內容是一個縮寫
注意點:
1. 常與全局屬性title配合使用,這樣可以在鼠標移動到該處時顯示全稱
`The <abbr title="People's Republic of China">PRC</abbr> was founded in 1949.`
2. 在瀏覽器內渲染會使其在內容底部加上短虛線
## `<address></address>`
作用:用于定義文檔或文章作者/擁有者的聯系信息
>如果 `<address>` 元素位于 `<body>` 元素內,則它表示文檔聯系信息。
>如果 `<address>` 元素位于 `<article>` 元素內,則是它表示文章的聯系信息。
注意點:
1. address元素中的文本通常呈現為斜體,大多數瀏覽器會在address元素前后換行
2. address元素不應該用于描述通訊地址,除非它是練習信息的一部分
3. address元素元素通常連同其他信息被包含在footer元素中
```
<address>
Written by <a href="mailto:webmaster@example.com">Donald Duck</a>.<br>
Visit us at:<br>
Example.com<br>
Box 564, Disneyland<br>
USA
</address>
```
## `<b></b>`
作用:定義粗體文本
## `<bdi></bdi>`
作用:定義文本的文本方向,使其脫離周圍文本的方向設置
注意點:
1. bdi指的是bidi隔離
2. 在發布用戶評論或其它難以控制的內容時,可以使用
3. 需要與全局屬性dir配合使用
## `<bdo></bdo>`
作用:定義文字方向
注意點:
1. 請與全局屬性dir配合使用
```
<p>
如果您的瀏覽器支持 bi-directional override (bdo),下一行會從右向左輸出 (rtl);
</p>
<bdo dir="rtl">
Here is some Hebrew text
</bdo>
```
## `<blockquote></blockquote>`
作用:`<blockquote>`標簽用于定義塊引用
注意點:
1. 標簽內的所有文本都會從常規文本中分離出來,一般會上下換行,左右增加外邊距,有時也會使用斜體
2. 換而言之,塊引用擁有它們自己的空間
3. 可選屬性:
`cite=url`規定引用來源
```
<blockquote cite="http://www.wwf.org">
WWF's ultimate goal is to build a future where people live in harmony with nature.
</blockquote>
```
主流瀏覽器均不支持cite屬性,但是搜索引擎可以因此獲得更多的信息
## `<q></q>`
作用:用于定義短引用
注意點:
1. 瀏覽器經常在引用內容的人左右添加引號
2. `<q>` 與 `<blockquote>` 的區別:
- `<q>` 標簽在本質上與 `<blockquote>` 是一樣的。不同之處在于它們的顯示和應用。`<q>` 標簽用于簡短的行內引用。如果需要從周圍內容分離出來比較長的部分(通常顯示為縮進地塊),請使用 `<blockquote>` 標簽。
3. 在html4中,firefox和opera中q元素包含的文本必須以引號來開始和結束,但是IE卻不支持這個規定,如果我們為了滿足其它瀏覽器而添加了引號,那么在IE中就會顯示兩組引號。
4. 盡管如此,我們還是推薦使用q元素,因為它在文檔處理和信息提取方面將會有很強的效果
5. 可選屬性:
`cite=citation`定義引用的出處或來源(citation)
## `<cite></cite>`
作用:表示所含文本是對某個參考文獻的引用
注意點:
1. 在顯示上與blockquote元素類似,均是斜體
2. 但是它不會有上下左右的外邊距
3. 通常情況下還要把引用包裹在一對`<a></a>`標簽中,然后把超鏈接指向引用
`<cite><a href=URL>引用名</a></cite>`
>`<cite>` 標簽還有一個隱藏的功能:它可以使你或者其他人從文檔中自動摘錄參考書目。我們可以很容易地想象一個瀏覽器,它能夠自動整理引用表格,并把它們作為腳注或者獨立的文檔來顯示。`<cite>` 標簽的語義已經遠遠超過了改變它所包含的文本外觀的作用;它使瀏覽器能夠以各種實用的方式來向用戶表達文檔的內容。
## `<code></code>`
作用:定義計算機代碼文本
注意點:
1. code元素并不能將元素內的內容以原樣顯示,瀏覽器仍然會解析內容而不跳過
2. code只是給內容的字體改為等寬字體,即它只是將內容轉變為暗示這是計算機代碼的內容
## `<var></var>`
作用:`<var>` 標簽表示變量的名稱,或者由用戶提供的值。
注意點:
1. 用 `<var>` 標簽標記的文本通常顯示為斜體。
2. `<var>` 標簽是計算機文檔中應用的另一個小竅門,這個標簽經常與 `<code>` 和 `<pre>` 標簽一起使用,用來顯示計算機編程代碼范例及類似方面的特定元素。
## `<smap></smap>`
作用:用于從一段上下文中抽取一些字符
例子:
`字符序列 <samp>ae</samp> 可能會被轉換為 æ 連字字符。`
效果:
`字符序列 ae 可能會被轉換為 ? 連字字符。`
## `<ins></ins>`
作用:定義一個插入文本
注意點:
1. 顯示效果是加入下劃線
## `<dfn></dfn>`
作用:用于標記特殊術語或短語
注意點:
1. 瀏覽器通常會將dfn元素內的內容顯示為斜體
2. 應當盡量少的使用,比如在技術性的文檔中,在第一次提到一個術語時,可以加上dfn元素,而在相同文檔的后續中,對于同一個術語,應避免使用dfn
## `<em></em>`
作用:定義一個強調文本
注意點:
1. 在顯示結果上,它依然是斜體
2. 如果你只是為了定義一個斜體的內容,可以考慮使用`<i></i>`或css樣式表
3. 對于強調的內容應當不宜過多,否則無法突出想要表達的內容
## `<strong></strong>`
作用:定義一個語氣更加強烈地強調文本
注意點:
1. 常識告訴我們應較少使用em元素的話,那么strong元素出現的次數應該更少,限制其使用可以讓這個標記更加的引人注意和有效
## `<i></i>`
作用:定義一個斜體文本或傾斜的文本
## `<kbd></kbd>`
作用:定義鍵盤文本
注意點:
1. 顯示效果為等寬字體
## `<mark></mark>`(HTML5)
作用:定義帶有記號的文本
注意點:
1. 與加粗不同,它會將文字加上背景色
## `<meter></meter>`(HTML5)
作用:定義已知范圍或分數值內的標量測量,也被稱為gauge(尺度)
注意點:
1. 這是一個html5的新標簽,假如你把文檔類型聲明為html4或以下和xhtml,標簽本身的效果將會受到影響
2. `<meter>`標簽不應用于指示進度條的進度,如果標記進度,請使用`<progress></progress>`標簽
可用屬性:
1. form=form_id-->規定meter元素所屬的一個或多個表單
2. high=number-->規定被視作高的值的范圍
3. low=number-->規定被視作高的值的范圍
4. max=number-->規定范圍的最大值
5. min=number-->規定范圍的最小值
6. optimum=number-->規定度量的優化值
7. value=number-->必須。規定度量的當前值
```
<meter value="3" min="0" max="10">十分之三</meter>
<meter value="0.6">60%</meter>
```
## `<progress></progress>`(HTML5)
作用:標示任務的進度(進程)
`<progress value="22" max="100"></progress> `
注意點:
1. 需要與js結合使用,來顯示任務的進度
2. progress標簽不適合使用來表示度量衡,這種情況請使用meter元素來替代
可用屬性:
max=number-->規定任務一共需要多少工作
value=number-->規定任務已經完成多少工作
## `<pre></pre>`
作用:用于定義預格式化的文本,即通常會保留空格和換行,文本會呈現為等寬字體
注意點:
1. pre元素常用于表示計算機的源代碼,但是計算機的源代碼(html)直接放入瀏覽器仍會解析(需要使用`<`和`>`等符號實體)
2. 會導致內容截斷的標簽絕不能包含在pre元素中,如標題、p元素、address元素
3. 可選的屬性:
`width=number`定義每行的最大字符數
## `<ruby></ruby>`
作用:可用于定義一個ruby注釋(中文注音或字符)
注意點:
1. ruby元素與rt元素一同使用
2. ruby元素由需要一組字符和一個提供信息的rt元素組成
3. 還包括一個可選的rp元素,定義瀏覽器不支持ruby元素時顯示的內容
## `<rt></rt>`
作用:定義字符的解釋或發音
## `<rp></rp>`
作用:定義瀏覽器不支持ruby元素時顯示的內容
## `<s></s>`
作用:定義加刪除線的文本
注意點:
1. `<s>`標簽是`<strike>`標簽的縮寫版本,但html4和xhtml中已經不再贊成使用它了,就是說,它早晚有一天會消失
2. 請使用`<del></del>`替代它
## `<del></del>`
作用:給元素中的內容上加上刪除線
注意點:
1. 請與`<ins></ins>`標簽配合使用,來描述文檔中的更新與修正
2. 可選的屬性:
- cite=URL
- datetime=YYYMMDD (定義文本被刪除的日期和時間)
## `<small></small>`
作用:標簽內的元素呈現小號字體的效果
注意點:
1. 如果被包裹字體已經是最小號的字體了,那這個標簽將不起任何作用
2. `<small></small>`是可以嵌套的,從而把文字連續的縮小,直到到達最小的一號字
## `<sup></sup>`
作用:標簽中的內容會以當前文本流中字符高度的一般來顯示(上標)
注意點:
1. 雖然顯示效果與文本流中其他元素不一樣,但是它們的字體字號都是一樣的
2. 這個標簽在向文檔添加注腳以及表示方程式中的指數時非常有效,如果與`<a></a>`標簽結合使用可以創建出很好的超鏈接注腳
## `<sub></sub>`
作用:標簽中的內容會以當前文本流中字符高度的一般來顯示(下標)
## `<template></template>`
作用:可以作為一個容器,但是它并不會存在于DOM樹中
注意點:
1. 多用于包裹一段代碼,對其綁定事件,使其可以控制是否隱藏
2. 一個檢查方法:
```
if (document.createElement("template").content) {
document.write("Your browser supports template!");
} else {
document.write("您的瀏覽器不支持 template!");
}
```
## `<u></u>`
作用:定義下劃線文本
注意點:
1. 應盡量避免使用,用戶可能會把它混淆為一個超鏈接
## `<time></time>`(HTML5)
作用:定義一個公歷的時間或日期,時間和時區偏移是可選的
可選的屬性:
1. datetime=datetime-->規定日期/時間。否則由元素內容給定日期時間
2. pubdate=pubdate-->指示 `<time>` 元素中的日期 / 時間是文檔(或 `<article>` 元素)的發布日期。
## `<wbr>`(HTML5)
一段帶有 Word Break Opportunity 的文本:
```
<p>
如果想學習 AJAX,那么您必須熟悉 XML<wbr>Http<wbr>Request 對象。
</p>
```
作用:Word Break Opportunity (`<wbr>`) 規定在文本中的何處適合添加換行符。
注意點:如果單詞太長,或者您擔心瀏覽器會在錯誤的位置換行,那么您可以使用 `<wbr>` 元素來添加 Word Break Opportunity(單詞換行時機)。
言:授人以魚不如授人以漁.先學會用,在學原理,在學創造,可能一輩子用不到這種能力,但是不能不具備這種能力。這篇文章主要是介紹算法入門Helloword之手寫圖片識別模型java中如何實現以及部分解釋。目前大家對于人工智能-機器學習-神經網絡的文章都是基于python語言的,對于擅長java的后端小伙伴想要去了解就不是特別友好,所以這里給大家介紹一下如何在java中實現,打開新世界的大門。以下為本人個人理解如有錯誤歡迎指正
在實現一個模型的時候我們要準備哪些知識體系:
1.機器學習基礎:包括監督學習、無監督學習、強化學習等基本概念。
2.數據處理與分析:數據清洗、特征工程、數據可視化等。
3.編程語言:如Python,用于實現機器學習算法。
4.數學基礎:線性代數、概率統計、微積分等數學知識。
5.機器學習算法:線性回歸、決策樹、神經網絡、支持向量機等算法。
6.深度學習框架:如TensorFlow、PyTorch等,用于構建和訓練深度學習模型。
7.模型評估與優化:交叉驗證、超參數調優、模型評估指標等。
8.實踐經驗:通過實際項目和競賽積累經驗,不斷提升模型學習能力。
這里的機器學習HelloWorld是手寫圖片識別用的是TensorFlow框架
主要需要:
1.理解手寫圖片的數據集,訓練集是什么樣的數據(60000,28,28) 、訓練集的標簽是什么樣的(1)
2.理解激活函數的作用
3.正向傳遞和反向傳播的作用以及實現
4.訓練模型和保存模型
5.加載保存的模型使用
因為python代碼解釋網上已經有很多了,這里不在重復解釋
def load_data(dpata_folder):
files = ["train-labels-idx1-ubyte.gz", "train-images-idx3-ubyte.gz",
"t10k-labels-idx1-ubyte.gz", "t10k-images-idx3-ubyte.gz"]
paths = []
for fname in files:
paths.append(os.path.join(data_folder, fname))
with gzip.open(paths[0], 'rb') as lbpath:
train_y = np.frombuffer(lbpath.read(), np.uint8, offset=8)
with gzip.open(paths[1], 'rb') as imgpath:
train_x = np.frombuffer(imgpath.read(), np.uint8, offset=16).reshape(len(train_y), 28, 28)
with gzip.open(paths[2], 'rb') as lbpath:
test_y = np.frombuffer(lbpath.read(), np.uint8, offset=8)
with gzip.open(paths[3], 'rb') as imgpath:
test_x = np.frombuffer(imgpath.read(), np.uint8, offset=16).reshape(len(test_y), 28, 28)
return (train_x, train_y), (test_x, test_y)
(train_x, train_y), (test_x, test_y) = load_data("mnistDataSet/")
print('\n train_x:%s, train_y:%s, test_x:%s, test_y:%s' % (train_x.shape, train_y.shape, test_x.shape, test_y.shape))
print(train_x.ndim) # 數據集的維度
print(train_x.shape) # 數據集的形狀
print(len(train_x)) # 數據集的大小
print(train_x) # 數據集
print("---查看單個數據")
print(train_x[0])
print(len(train_x[0]))
print(len(train_x[0][1]))
print(train_x[0][6])
print("---查看單個數據")
print(train_y[3])
SimpleMnist.class
private static final String TRAINING_IMAGES_ARCHIVE = "mnist/train-images-idx3-ubyte.gz";
private static final String TRAINING_LABELS_ARCHIVE = "mnist/train-labels-idx1-ubyte.gz";
private static final String TEST_IMAGES_ARCHIVE = "mnist/t10k-images-idx3-ubyte.gz";
private static final String TEST_LABELS_ARCHIVE = "mnist/t10k-labels-idx1-ubyte.gz";
//加載數據
MnistDataset validationDataset = MnistDataset.getOneValidationImage(3, TRAINING_IMAGES_ARCHIVE, TRAINING_LABELS_ARCHIVE,TEST_IMAGES_ARCHIVE, TEST_LABELS_ARCHIVE);
MnistDataset.class
/**
* @param trainingImagesArchive 訓練圖片路徑
* @param trainingLabelsArchive 訓練標簽路徑
* @param testImagesArchive 測試圖片路徑
* @param testLabelsArchive 測試標簽路徑
*/
public static MnistDataset getOneValidationImage(int index, String trainingImagesArchive, String trainingLabelsArchive,String testImagesArchive, String testLabelsArchive) {
try {
ByteNdArray trainingImages = readArchive(trainingImagesArchive);
ByteNdArray trainingLabels = readArchive(trainingLabelsArchive);
ByteNdArray testImages = readArchive(testImagesArchive);
ByteNdArray testLabels = readArchive(testLabelsArchive);
trainingImages.slice(sliceFrom(0));
trainingLabels.slice(sliceTo(0));
// 切片操作
Index range = Indices.range(index, index + 1);// 切片的起始和結束索引
ByteNdArray validationImage = trainingImages.slice(range); // 執行切片操作
ByteNdArray validationLable = trainingLabels.slice(range); // 執行切片操作
if (index >= 0) {
return new MnistDataset(trainingImages,trainingLabels,validationImage,validationLable,testImages,testLabels);
} else {
return null;
}
} catch (IOException e) {
throw new AssertionError(e);
}
}
private static ByteNdArray readArchive(String archiveName) throws IOException {
System.out.println("archiveName = " + archiveName);
DataInputStream archiveStream = new DataInputStream(new GZIPInputStream(MnistDataset.class.getClassLoader().getResourceAsStream(archiveName))
);
archiveStream.readShort(); // first two bytes are always 0
byte magic = archiveStream.readByte();
if (magic != TYPE_UBYTE) {
throw new IllegalArgumentException("\"" + archiveName + "\" is not a valid archive");
}
int numDims = archiveStream.readByte();
long[] dimSizes = new long[numDims];
int size = 1; // for simplicity, we assume that total size does not exceeds Integer.MAX_VALUE
for (int i = 0; i < dimSizes.length; ++i) {
dimSizes[i] = archiveStream.readInt();
size *= dimSizes[i];
}
byte[] bytes = new byte[size];
archiveStream.readFully(bytes);
return NdArrays.wrap(Shape.of(dimSizes), DataBuffers.of(bytes, false, false));
}
/**
* Mnist 數據集構造器
*/
private MnistDataset(ByteNdArray trainingImages, ByteNdArray trainingLabels,ByteNdArray validationImages,ByteNdArray validationLabels,ByteNdArray testImages,ByteNdArray testLabels
) {
this.trainingImages = trainingImages;
this.trainingLabels = trainingLabels;
this.validationImages = validationImages;
this.validationLabels = validationLabels;
this.testImages = testImages;
this.testLabels = testLabels;
this.imageSize = trainingImages.get(0).shape().size();
System.out.println(String.format("train_x:%s,train_y:%s, test_x:%s, test_y:%s", trainingImages.shape(), trainingLabels.shape(), testImages.shape(), testLabels.shape()));
System.out.println("數據集的維度:" + trainingImages.rank());
System.out.println("數據集的形狀 = " + trainingImages.shape());
System.out.println("數據集的大小 = " + trainingImages.shape().get(0));
System.out.println("查看單個數據 = " + trainingImages.get(0));
}
model = tensorflow.keras.Sequential()
model.add(tensorflow.keras.layers.Flatten(input_shape=(28, 28))) # 添加Flatten層說明輸入數據的形狀
model.add(tensorflow.keras.layers.Dense(128, activation='relu')) # 添加隱含層,為全連接層,128個節點,relu激活函數
model.add(tensorflow.keras.layers.Dense(10, activation='softmax')) # 添加輸出層,為全連接層,10個節點,softmax激活函數
print("打印模型結構")
# 使用 summary 打印模型結構
print('\n', model.summary()) # 查看網絡結構和參數信息
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['sparse_categorical_accuracy'])
SimpleMnist.class
Ops tf = Ops.create(graph);
// Create placeholders and variables, which should fit batches of an unknown number of images
//創建占位符和變量,這些占位符和變量應適合未知數量的圖像批次
Placeholder<TFloat32> images = tf.placeholder(TFloat32.class);
Placeholder<TFloat32> labels = tf.placeholder(TFloat32.class);
// Create weights with an initial value of 0
// 創建初始值為 0 的權重
Shape weightShape = Shape.of(dataset.imageSize(), MnistDataset.NUM_CLASSES);
Variable<TFloat32> weights = tf.variable(tf.zeros(tf.constant(weightShape), TFloat32.class));
// Create biases with an initial value of 0
//創建初始值為 0 的偏置
Shape biasShape = Shape.of(MnistDataset.NUM_CLASSES);
Variable<TFloat32> biases = tf.variable(tf.zeros(tf.constant(biasShape), TFloat32.class));
// Predict the class of each image in the batch and compute the loss
//使用 TensorFlow 的 tf.linalg.matMul 函數計算圖像矩陣 images 和權重矩陣 weights 的矩陣乘法,并加上偏置項 biases。
//wx+b
MatMul<TFloat32> matMul = tf.linalg.matMul(images, weights);
Add<TFloat32> add = tf.math.add(matMul, biases);
//Softmax 是一個常用的激活函數,它將輸入轉換為表示概率分布的輸出。對于輸入向量中的每個元素,Softmax 函數會計算指數,
//并對所有元素求和,然后將每個元素的指數除以總和,最終得到一個概率分布。這通常用于多分類問題,以輸出每個類別的概率
Softmax<TFloat32> softmax = tf.nn.softmax(add);
// 創建一個計算交叉熵的Mean對象
Mean<TFloat32> crossEntropy =
tf.math.mean( // 計算張量的平均值
tf.math.neg( // 計算張量的負值
tf.reduceSum( // 計算張量的和
tf.math.mul(labels, tf.math.log(softmax)), //計算標簽和softmax預測的對數乘積
tf.array(1) // 在指定軸上求和
)
),
tf.array(0) // 在指定軸上求平均值
);
// Back-propagate gradients to variables for training
//使用梯度下降優化器來最小化交叉熵損失函數。首先,創建了一個梯度下降優化器 optimizer,然后使用該優化器來最小化交叉熵損失函數 crossEntropy。
Optimizer optimizer = new GradientDescent(graph, LEARNING_RATE);
Op minimize = optimizer.minimize(crossEntropy);
history = model.fit(train_x, train_y, batch_size=64, epochs=5, validation_split=0.2)
SimpleMnist.class
// Train the model
for (ImageBatch trainingBatch : dataset.trainingBatches(TRAINING_BATCH_SIZE)) {
try (TFloat32 batchImages = preprocessImages(trainingBatch.images());
TFloat32 batchLabels = preprocessLabels(trainingBatch.labels())) {
// 創建會話運行器
session.runner()
// 添加要最小化的目標
.addTarget(minimize)
// 通過feed方法將圖像數據輸入到模型中
.feed(images.asOutput(), batchImages)
// 通過feed方法將標簽數據輸入到模型中
.feed(labels.asOutput(), batchLabels)
// 運行會話
.run();
}
}
test_loss, test_acc = model.evaluate(test_x, test_y)
model.evaluate(test_x, test_y, verbose=2) # 每次迭代輸出一條記錄,來評價該模型是否有比較好的泛化能力
print('Test 損失: %.3f' % test_loss)
print('Test 精確度: %.3f' % test_acc)
java中
SimpleMnist.class
// Test the model
ImageBatch testBatch = dataset.testBatch();
try (TFloat32 testImages = preprocessImages(testBatch.images());
TFloat32 testLabels = preprocessLabels(testBatch.labels());
// 定義一個TFloat32類型的變量accuracyValue,用于存儲計算得到的準確率值
TFloat32 accuracyValue = (TFloat32) session.runner()
// 從會話中獲取準確率值
.fetch(accuracy)
.fetch(predicted)
.fetch(expected)
// 將images作為輸入,testImages作為數據進行喂養
.feed(images.asOutput(), testImages)
// 將labels作為輸入,testLabels作為數據進行喂養
.feed(labels.asOutput(), testLabels)
// 運行會話并獲取結果
.run()
// 獲取第一個結果并存儲在accuracyValue中
.get(0)) {
System.out.println("Accuracy: " + accuracyValue.getFloat());
}
# 使用save_model保存完整模型
# save_model(model, '/media/cfs/用戶ERP名稱/ea/saved_model', save_format='pb')
save_model(model, 'D:\\pythonProject\\mnistDemo\\number_model', save_format='pb')
SimpleMnist.class
// 保存模型
SavedModelBundle.Exporter exporter = SavedModelBundle.exporter("D:\\ai\\ai-demo").withSession(session);
Signature.Builder builder = Signature.builder();
builder.input("images", images);
builder.input("labels", labels);
builder.output("accuracy", accuracy);
builder.output("expected", expected);
builder.output("predicted", predicted);
Signature signature = builder.build();
SessionFunction sessionFunction = SessionFunction.create(signature, session);
exporter.withFunction(sessionFunction);
exporter.export();
# 加載.pb模型文件
global load_model
load_model = load_model('D:\\pythonProject\\mnistDemo\\number_model')
load_model.summary()
demo = tensorflow.reshape(test_x, (1, 28, 28))
input_data = np.array(demo) # 準備你的輸入數據
input_data = tensorflow.convert_to_tensor(input_data, dtype=tensorflow.float32)
predictValue = load_model.predict(input_data)
print("predictValue")
print(predictValue)
y_pred = np.argmax(predictValue)
print('標簽值:' + str(test_y) + '\n預測值:' + str(y_pred))
return y_pred, test_y,
SimpleMnist.class
//加載模型并預測
public void loadModel(String exportDir) {
// load saved model
SavedModelBundle model = SavedModelBundle.load(exportDir, "serve");
try {
printSignature(model);
} catch (Exception e) {
throw new RuntimeException(e);
}
ByteNdArray validationImages = dataset.getValidationImages();
ByteNdArray validationLabels = dataset.getValidationLabels();
TFloat32 testImages = preprocessImages(validationImages);
System.out.println("testImages = " + testImages.shape());
TFloat32 testLabels = preprocessLabels(validationLabels);
System.out.println("testLabels = " + testLabels.shape());
Result run = model.session().runner()
.feed("Placeholder:0", testImages)
.feed("Placeholder_1:0", testLabels)
.fetch("ArgMax:0")
.fetch("ArgMax_1:0")
.fetch("Mean_1:0")
.run();
// 處理輸出
Optional<Tensor> tensor1 = run.get("ArgMax:0");
Optional<Tensor> tensor2 = run.get("ArgMax_1:0");
Optional<Tensor> tensor3 = run.get("Mean_1:0");
TInt64 predicted = (TInt64) tensor1.get();
Long predictedValue = predicted.getObject(0);
System.out.println("predictedValue = " + predictedValue);
TInt64 expected = (TInt64) tensor2.get();
Long expectedValue = expected.getObject(0);
System.out.println("expectedValue = " + expectedValue);
TFloat32 accuracy = (TFloat32) tensor3.get();
System.out.println("accuracy = " + accuracy.getFloat());
}
//打印模型信息
private static void printSignature(SavedModelBundle model) throws Exception {
MetaGraphDef m = model.metaGraphDef();
SignatureDef sig = m.getSignatureDefOrThrow("serving_default");
int numInputs = sig.getInputsCount();
int i = 1;
System.out.println("MODEL SIGNATURE");
System.out.println("Inputs:");
for (Map.Entry<String, TensorInfo> entry : sig.getInputsMap().entrySet()) {
TensorInfo t = entry.getValue();
System.out.printf(
"%d of %d: %-20s (Node name in graph: %-20s, type: %s)\n",
i++, numInputs, entry.getKey(), t.getName(), t.getDtype());
}
int numOutputs = sig.getOutputsCount();
i = 1;
System.out.println("Outputs:");
for (Map.Entry<String, TensorInfo> entry : sig.getOutputsMap().entrySet()) {
TensorInfo t = entry.getValue();
System.out.printf(
"%d of %d: %-20s (Node name in graph: %-20s, type: %s)\n",
i++, numOutputs, entry.getKey(), t.getName(), t.getDtype());
}
}
本工程使用環境為
Python: 3.7.9
https://www.python.org/downloads/windows/
Anaconda: Python 3.11 Anaconda3-2023.09-0-Windows-x86_64
https://www.anaconda.com/download#downloads
tensorflow:2.0.0
直接從anaconda下安裝
import gzip
import os.path
import tensorflow as tensorflow
from tensorflow import keras
# 可視化 image
import matplotlib.pyplot as plt
import numpy as np
from tensorflow.keras.models import save_model
# 加載數據
# mnist = keras.datasets.mnist
# mnistData = mnist.load_data() #Exception: URL fetch failure on https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz: None -- unknown url type: https
"""
這里可以直接使用
mnist = keras.datasets.mnist
mnistData = mnist.load_data() 加載數據,但是有的時候不成功,所以使用本地加載數據
"""
def load_data(data_folder):
files = ["train-labels-idx1-ubyte.gz", "train-images-idx3-ubyte.gz",
"t10k-labels-idx1-ubyte.gz", "t10k-images-idx3-ubyte.gz"]
paths = []
for fname in files:
paths.append(os.path.join(data_folder, fname))
with gzip.open(paths[0], 'rb') as lbpath:
train_y = np.frombuffer(lbpath.read(), np.uint8, offset=8)
with gzip.open(paths[1], 'rb') as imgpath:
train_x = np.frombuffer(imgpath.read(), np.uint8, offset=16).reshape(len(train_y), 28, 28)
with gzip.open(paths[2], 'rb') as lbpath:
test_y = np.frombuffer(lbpath.read(), np.uint8, offset=8)
with gzip.open(paths[3], 'rb') as imgpath:
test_x = np.frombuffer(imgpath.read(), np.uint8, offset=16).reshape(len(test_y), 28, 28)
return (train_x, train_y), (test_x, test_y)
(train_x, train_y), (test_x, test_y) = load_data("mnistDataSet/")
print('\n train_x:%s, train_y:%s, test_x:%s, test_y:%s' % (train_x.shape, train_y.shape, test_x.shape, test_y.shape))
print(train_x.ndim) # 數據集的維度
print(train_x.shape) # 數據集的形狀
print(len(train_x)) # 數據集的大小
print(train_x) # 數據集
print("---查看單個數據")
print(train_x[0])
print(len(train_x[0]))
print(len(train_x[0][1]))
print(train_x[0][6])
# 可視化image圖片、一副image的數據
# plt.imshow(train_x[0].reshape(28, 28), cmap="binary")
# plt.show()
print("---查看單個數據")
print(train_y[0])
# 數據預處理
# 歸一化、并轉換為tensor張量,數據類型為float32. ---歸一化也可能造成識別率低
# train_x, test_x = tensorflow.cast(train_x / 255.0, tensorflow.float32), tensorflow.cast(test_x / 255.0,
# tensorflow.float32),
# train_y, test_y = tensorflow.cast(train_y, tensorflow.int16), tensorflow.cast(test_y, tensorflow.int16)
# print("---查看單個數據歸一后的數據")
# print(train_x[0][6]) # 30/255=0.11764706 ---歸一化每個值除以255
# print(train_y[0])
# Step2: 配置網絡 建立模型
'''
以下的代碼判斷就是定義一個簡單的多層感知器,一共有三層,
兩個大小為100的隱層和一個大小為10的輸出層,因為MNIST數據集是手寫0到9的灰度圖像,
類別有10個,所以最后的輸出大小是10。最后輸出層的激活函數是Softmax,
所以最后的輸出層相當于一個分類器。加上一個輸入層的話,
多層感知器的結構是:輸入層-->>隱層-->>隱層-->>輸出層。
激活函數 https://zhuanlan.zhihu.com/p/337902763
'''
# 構造模型
# model = keras.Sequential([
# # 在第一層的網絡中,我們的輸入形狀是28*28,這里的形狀就是圖片的長度和寬度。
# keras.layers.Flatten(input_shape=(28, 28)),
# # 所以神經網絡有點像濾波器(過濾裝置),輸入一組28*28像素的圖片后,輸出10個類別的判斷結果。那這個128的數字是做什么用的呢?
# # 我們可以這樣想象,神經網絡中有128個函數,每個函數都有自己的參數。
# # 我們給這些函數進行一個編號,f0,f1…f127 ,我們想的是當圖片的像素一一帶入這128個函數后,這些函數的組合最終輸出一個標簽值,在這個樣例中,我們希望它輸出09 。
# # 為了得到這個結果,計算機必須要搞清楚這128個函數的具體參數,之后才能計算各個圖片的標簽。這里的邏輯是,一旦計算機搞清楚了這些參數,那它就能夠認出不同的10個類別的事物了。
# keras.layers.Dense(100, activation=tensorflow.nn.relu),
# # 最后一層是10,是數據集中各種類別的代號,數據集總共有10類,這里就是10 。
# keras.layers.Dense(10, activation=tensorflow.nn.softmax)
# ])
model = tensorflow.keras.Sequential()
model.add(tensorflow.keras.layers.Flatten(input_shape=(28, 28))) # 添加Flatten層說明輸入數據的形狀
model.add(tensorflow.keras.layers.Dense(128, activation='relu')) # 添加隱含層,為全連接層,128個節點,relu激活函數
model.add(tensorflow.keras.layers.Dense(10, activation='softmax')) # 添加輸出層,為全連接層,10個節點,softmax激活函數
print("打印模型結構")
# 使用 summary 打印模型結構
# print(model.summary())
print('\n', model.summary()) # 查看網絡結構和參數信息
'''
接著是配置模型,在這一步,我們需要指定模型訓練時所使用的優化算法與損失函數,
此外,這里我們也可以定義計算精度相關的API。
優化器https://zhuanlan.zhihu.com/p/27449596
'''
# 配置模型 配置模型訓練方法
# 設置神經網絡的優化器和損失函數。# 使用Adam算法進行優化 # 使用CrossEntropyLoss 計算損失 # 使用Accuracy 計算精度
# model.compile(optimizer=tensorflow.optimizers.Adam(), loss='sparse_categorical_crossentropy', metrics=['accuracy'])
# adam算法參數采用keras默認的公開參數,損失函數采用稀疏交叉熵損失函數,準確率采用稀疏分類準確率函數
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['sparse_categorical_accuracy'])
# Step3:模型訓練
# 開始模型訓練
# model.fit(x_train, # 設置訓練數據集
# y_train,
# epochs=5, # 設置訓練輪數
# batch_size=64, # 設置 batch_size
# verbose=1) # 設置日志打印格式
# 批量訓練大小為64,迭代5次,測試集比例0.2(48000條訓練集數據,12000條測試集數據)
history = model.fit(train_x, train_y, batch_size=64, epochs=5, validation_split=0.2)
# STEP4: 模型評估
# 評估模型,不輸出預測結果輸出損失和精確度. test_loss損失,test_acc精確度
test_loss, test_acc = model.evaluate(test_x, test_y)
model.evaluate(test_x, test_y, verbose=2) # 每次迭代輸出一條記錄,來評價該模型是否有比較好的泛化能力
# model.evaluate(test_dataset, verbose=1)
print('Test 損失: %.3f' % test_loss)
print('Test 精確度: %.3f' % test_acc)
# 結果可視化
print(history.history)
loss = history.history['loss'] # 訓練集損失
val_loss = history.history['val_loss'] # 測試集損失
acc = history.history['sparse_categorical_accuracy'] # 訓練集準確率
val_acc = history.history['val_sparse_categorical_accuracy'] # 測試集準確率
plt.figure(figsize=(10, 3))
plt.subplot(121)
plt.plot(loss, color='b', label='train')
plt.plot(val_loss, color='r', label='test')
plt.ylabel('loss')
plt.legend()
plt.subplot(122)
plt.plot(acc, color='b', label='train')
plt.plot(val_acc, color='r', label='test')
plt.ylabel('Accuracy')
plt.legend()
# 暫停5秒關閉畫布,否則畫布一直打開的同時,會持續占用GPU內存
# plt.ion() # 打開交互式操作模式
# plt.show()
# plt.pause(5)
# plt.close()
# plt.show()
# Step5:模型預測 輸入測試數據,輸出預測結果
for i in range(1):
num = np.random.randint(1, 10000) # 在1~10000之間生成隨機整數
plt.subplot(2, 5, i + 1)
plt.axis('off')
plt.imshow(test_x[num], cmap='gray')
demo = tensorflow.reshape(test_x[num], (1, 28, 28))
y_pred = np.argmax(model.predict(demo))
plt.title('標簽值:' + str(test_y[num]) + '\n預測值:' + str(y_pred))
# plt.show()
'''
保存模型
訓練好的模型可以用于加載后對新輸入數據進行預測,所以需要先進行保存已訓練模型
'''
#使用save_model保存完整模型
save_model(model, 'D:\\pythonProject\\mnistDemo\\number_model', save_format='pb')
import numpy as np
import tensorflow as tensorflow
import gzip
import os.path
from tensorflow.keras.models import load_model
# 預測
def predict(test_x, test_y):
test_x, test_y = test_x, test_y
'''
五、模型評估
需要先加載已訓練模型,然后用其預測新的數據,計算評估指標
'''
# 模型加載
# 加載.pb模型文件
global load_model
# load_model = load_model('./saved_model')
load_model = load_model('D:\\pythonProject\\mnistDemo\\number_model')
load_model.summary()
# make a prediction
print("test_x")
print(test_x)
print(test_x.ndim)
print(test_x.shape)
demo = tensorflow.reshape(test_x, (1, 28, 28))
input_data = np.array(demo) # 準備你的輸入數據
input_data = tensorflow.convert_to_tensor(input_data, dtype=tensorflow.float32)
# test_x = tensorflow.cast(test_x / 255.0, tensorflow.float32)
# test_y = tensorflow.cast(test_y, tensorflow.int16)
predictValue = load_model.predict(input_data)
print("predictValue")
print(predictValue)
y_pred = np.argmax(predictValue)
print('標簽值:' + str(test_y) + '\n預測值:' + str(y_pred))
return y_pred, test_y,
def load_data(data_folder):
files = ["train-labels-idx1-ubyte.gz", "train-images-idx3-ubyte.gz",
"t10k-labels-idx1-ubyte.gz", "t10k-images-idx3-ubyte.gz"]
paths = []
for fname in files:
paths.append(os.path.join(data_folder, fname))
with gzip.open(paths[0], 'rb') as lbpath:
train_y = np.frombuffer(lbpath.read(), np.uint8, offset=8)
with gzip.open(paths[1], 'rb') as imgpath:
train_x = np.frombuffer(imgpath.read(), np.uint8, offset=16).reshape(len(train_y), 28, 28)
with gzip.open(paths[2], 'rb') as lbpath:
test_y = np.frombuffer(lbpath.read(), np.uint8, offset=8)
with gzip.open(paths[3], 'rb') as imgpath:
test_x = np.frombuffer(imgpath.read(), np.uint8, offset=16).reshape(len(test_y), 28, 28)
return (train_x, train_y), (test_x, test_y)
(train_x, train_y), (test_x, test_y) = load_data("mnistDataSet/")
print(train_x[0])
predict(train_x[0], train_y)
tensorflow 需要的java 版本對應表: https://github.com/tensorflow/java/#tensorflow-version-support
本工程使用環境為
jdk版本:openjdk-21
pom依賴如下:
<dependency>
<groupId>org.tensorflow</groupId>
<artifactId>tensorflow-core-platform</artifactId>
<version>0.6.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.tensorflow</groupId>
<artifactId>tensorflow-framework</artifactId>
<version>0.6.0-SNAPSHOT</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>tensorflow-snapshots</id>
<url>https://oss.sonatype.org/content/repositories/snapshots/</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
數據集創建和解析類
package org.example.tensorDemo.datasets.mnist;
import org.example.tensorDemo.datasets.ImageBatch;
import org.example.tensorDemo.datasets.ImageBatchIterator;
import org.tensorflow.ndarray.*;
import org.tensorflow.ndarray.buffer.DataBuffers;
import org.tensorflow.ndarray.index.Index;
import org.tensorflow.ndarray.index.Indices;
import java.io.DataInputStream;
import java.io.IOException;
import java.util.zip.GZIPInputStream;
import static org.tensorflow.ndarray.index.Indices.sliceFrom;
import static org.tensorflow.ndarray.index.Indices.sliceTo;
public class MnistDataset {
public static final int NUM_CLASSES = 10;
private static final int TYPE_UBYTE = 0x08;
/**
* 訓練圖片字節類型的多維數組
*/
private final ByteNdArray trainingImages;
/**
* 訓練標簽字節類型的多維數組
*/
private final ByteNdArray trainingLabels;
/**
* 驗證圖片字節類型的多維數組
*/
public final ByteNdArray validationImages;
/**
* 驗證標簽字節類型的多維數組
*/
public final ByteNdArray validationLabels;
/**
* 測試圖片字節類型的多維數組
*/
private final ByteNdArray testImages;
/**
* 測試標簽字節類型的多維數組
*/
private final ByteNdArray testLabels;
/**
* 圖片的大小
*/
private final long imageSize;
/**
* Mnist 數據集構造器
*/
private MnistDataset(
ByteNdArray trainingImages,
ByteNdArray trainingLabels,
ByteNdArray validationImages,
ByteNdArray validationLabels,
ByteNdArray testImages,
ByteNdArray testLabels
) {
this.trainingImages = trainingImages;
this.trainingLabels = trainingLabels;
this.validationImages = validationImages;
this.validationLabels = validationLabels;
this.testImages = testImages;
this.testLabels = testLabels;
//第一個圖像的形狀,并返回其尺寸大小。每一張圖片包含28X28個像素點 所以應該為784
this.imageSize = trainingImages.get(0).shape().size();
// System.out.println("imageSize = " + imageSize);
// System.out.println(String.format("train_x:%s,train_y:%s, test_x:%s, test_y:%s", trainingImages.shape(), trainingLabels.shape(), testImages.shape(), testLabels.shape()));
// System.out.println("數據集的維度:" + trainingImages.rank());
// System.out.println("數據集的形狀 = " + trainingImages.shape());
// System.out.println("數據集的大小 = " + trainingImages.shape().get(0));
// System.out.println("數據集 = ");
// for (int i = 0; i < trainingImages.shape().get(0); i++) {
// for (int j = 0; j < trainingImages.shape().get(1); j++) {
// for (int k = 0; k < trainingImages.shape().get(2); k++) {
// System.out.print(trainingImages.getObject(i, j, k) + " ");
// }
// System.out.println();
// }
// System.out.println();
// }
// System.out.println("查看單個數據 = " + trainingImages.get(0));
// for (int j = 0; j < trainingImages.shape().get(1); j++) {
// for (int k = 0; k < trainingImages.shape().get(2); k++) {
// System.out.print(trainingImages.getObject(0, j, k) + " ");
// }
// System.out.println();
// }
// System.out.println("查看單個數據大小 = " + trainingImages.get(0).size());
// System.out.println("查看trainingImages三維數組下的第一個元素的第二個二維數組大小 = " + trainingImages.get(0).get(1).size());
// System.out.println("查看trainingImages三維數組下的第一個元素的第7個二維數組的第8個元素 = " + trainingImages.getObject(0, 6, 8));
// System.out.println("trainingLabels = " + trainingLabels.getObject(1));
}
/**
* @param validationSize 驗證的數據
* @param trainingImagesArchive 訓練圖片路徑
* @param trainingLabelsArchive 訓練標簽路徑
* @param testImagesArchive 測試圖片路徑
* @param testLabelsArchive 測試標簽路徑
*/
public static MnistDataset create(int validationSize, String trainingImagesArchive, String trainingLabelsArchive,
String testImagesArchive, String testLabelsArchive) {
try {
ByteNdArray trainingImages = readArchive(trainingImagesArchive);
ByteNdArray trainingLabels = readArchive(trainingLabelsArchive);
ByteNdArray testImages = readArchive(testImagesArchive);
ByteNdArray testLabels = readArchive(testLabelsArchive);
if (validationSize > 0) {
return new MnistDataset(
trainingImages.slice(sliceFrom(validationSize)),
trainingLabels.slice(sliceFrom(validationSize)),
trainingImages.slice(sliceTo(validationSize)),
trainingLabels.slice(sliceTo(validationSize)),
testImages,
testLabels
);
}
return new MnistDataset(trainingImages, trainingLabels, null, null, testImages, testLabels);
} catch (IOException e) {
throw new AssertionError(e);
}
}
/**
* @param trainingImagesArchive 訓練圖片路徑
* @param trainingLabelsArchive 訓練標簽路徑
* @param testImagesArchive 測試圖片路徑
* @param testLabelsArchive 測試標簽路徑
*/
public static MnistDataset getOneValidationImage(int index, String trainingImagesArchive, String trainingLabelsArchive,
String testImagesArchive, String testLabelsArchive) {
try {
ByteNdArray trainingImages = readArchive(trainingImagesArchive);
ByteNdArray trainingLabels = readArchive(trainingLabelsArchive);
ByteNdArray testImages = readArchive(testImagesArchive);
ByteNdArray testLabels = readArchive(testLabelsArchive);
trainingImages.slice(sliceFrom(0));
trainingLabels.slice(sliceTo(0));
// 切片操作
Index range = Indices.range(index, index + 1);// 切片的起始和結束索引
ByteNdArray validationImage = trainingImages.slice(range); // 執行切片操作
ByteNdArray validationLable = trainingLabels.slice(range); // 執行切片操作
if (index >= 0) {
return new MnistDataset(
trainingImages,
trainingLabels,
validationImage,
validationLable,
testImages,
testLabels
);
} else {
return null;
}
} catch (IOException e) {
throw new AssertionError(e);
}
}
private static ByteNdArray readArchive(String archiveName) throws IOException {
System.out.println("archiveName = " + archiveName);
DataInputStream archiveStream = new DataInputStream(
//new GZIPInputStream(new java.io.FileInputStream("src/main/resources/"+archiveName))
new GZIPInputStream(MnistDataset.class.getClassLoader().getResourceAsStream(archiveName))
);
//todo 不知道怎么讀取和實際的內部結構
archiveStream.readShort(); // first two bytes are always 0
byte magic = archiveStream.readByte();
if (magic != TYPE_UBYTE) {
throw new IllegalArgumentException("\"" + archiveName + "\" is not a valid archive");
}
int numDims = archiveStream.readByte();
long[] dimSizes = new long[numDims];
int size = 1; // for simplicity, we assume that total size does not exceeds Integer.MAX_VALUE
for (int i = 0; i < dimSizes.length; ++i) {
dimSizes[i] = archiveStream.readInt();
size *= dimSizes[i];
}
byte[] bytes = new byte[size];
archiveStream.readFully(bytes);
return NdArrays.wrap(Shape.of(dimSizes), DataBuffers.of(bytes, false, false));
}
public Iterable<ImageBatch> trainingBatches(int batchSize) {
return () -> new ImageBatchIterator(batchSize, trainingImages, trainingLabels);
}
public Iterable<ImageBatch> validationBatches(int batchSize) {
return () -> new ImageBatchIterator(batchSize, validationImages, validationLabels);
}
public Iterable<ImageBatch> testBatches(int batchSize) {
return () -> new ImageBatchIterator(batchSize, testImages, testLabels);
}
public ImageBatch testBatch() {
return new ImageBatch(testImages, testLabels);
}
public long imageSize() {
return imageSize;
}
public long numTrainingExamples() {
return trainingLabels.shape().size(0);
}
public long numTestingExamples() {
return testLabels.shape().size(0);
}
public long numValidationExamples() {
return validationLabels.shape().size(0);
}
public ByteNdArray getValidationImages() {
return validationImages;
}
public ByteNdArray getValidationLabels() {
return validationLabels;
}
}
package org.example.tensorDemo.dense;
import org.example.tensorDemo.datasets.ImageBatch;
import org.example.tensorDemo.datasets.mnist.MnistDataset;
import org.tensorflow.*;
import org.tensorflow.framework.optimizers.GradientDescent;
import org.tensorflow.framework.optimizers.Optimizer;
import org.tensorflow.ndarray.ByteNdArray;
import org.tensorflow.ndarray.Shape;
import org.tensorflow.op.Op;
import org.tensorflow.op.Ops;
import org.tensorflow.op.core.Placeholder;
import org.tensorflow.op.core.Variable;
import org.tensorflow.op.linalg.MatMul;
import org.tensorflow.op.math.Add;
import org.tensorflow.op.math.Mean;
import org.tensorflow.op.nn.Softmax;
import org.tensorflow.proto.framework.MetaGraphDef;
import org.tensorflow.proto.framework.SignatureDef;
import org.tensorflow.proto.framework.TensorInfo;
import org.tensorflow.types.TFloat32;
import org.tensorflow.types.TInt64;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
public class SimpleMnist implements Runnable {
private static final String TRAINING_IMAGES_ARCHIVE = "mnist/train-images-idx3-ubyte.gz";
private static final String TRAINING_LABELS_ARCHIVE = "mnist/train-labels-idx1-ubyte.gz";
private static final String TEST_IMAGES_ARCHIVE = "mnist/t10k-images-idx3-ubyte.gz";
private static final String TEST_LABELS_ARCHIVE = "mnist/t10k-labels-idx1-ubyte.gz";
public static void main(String[] args) {
//加載數據集
// MnistDataset dataset = MnistDataset.create(VALIDATION_SIZE, TRAINING_IMAGES_ARCHIVE, TRAINING_LABELS_ARCHIVE,
// TEST_IMAGES_ARCHIVE, TEST_LABELS_ARCHIVE);
MnistDataset validationDataset = MnistDataset.getOneValidationImage(3, TRAINING_IMAGES_ARCHIVE, TRAINING_LABELS_ARCHIVE,
TEST_IMAGES_ARCHIVE, TEST_LABELS_ARCHIVE);
//創建了一個名為graph的圖形對象。
try (Graph graph = new Graph()) {
SimpleMnist mnist = new SimpleMnist(graph, validationDataset);
mnist.run();//構建和訓練模型
mnist.loadModel("D:\\ai\\ai-demo");
}
}
@Override
public void run() {
Ops tf = Ops.create(graph);
// Create placeholders and variables, which should fit batches of an unknown number of images
//創建占位符和變量,這些占位符和變量應適合未知數量的圖像批次
Placeholder<TFloat32> images = tf.placeholder(TFloat32.class);
Placeholder<TFloat32> labels = tf.placeholder(TFloat32.class);
// Create weights with an initial value of 0
// 創建初始值為 0 的權重
Shape weightShape = Shape.of(dataset.imageSize(), MnistDataset.NUM_CLASSES);
Variable<TFloat32> weights = tf.variable(tf.zeros(tf.constant(weightShape), TFloat32.class));
// Create biases with an initial value of 0
//創建初始值為 0 的偏置
Shape biasShape = Shape.of(MnistDataset.NUM_CLASSES);
Variable<TFloat32> biases = tf.variable(tf.zeros(tf.constant(biasShape), TFloat32.class));
// Predict the class of each image in the batch and compute the loss
//使用 TensorFlow 的 tf.linalg.matMul 函數計算圖像矩陣 images 和權重矩陣 weights 的矩陣乘法,并加上偏置項 biases。
//wx+b
MatMul<TFloat32> matMul = tf.linalg.matMul(images, weights);
Add<TFloat32> add = tf.math.add(matMul, biases);
//Softmax 是一個常用的激活函數,它將輸入轉換為表示概率分布的輸出。對于輸入向量中的每個元素,Softmax 函數會計算指數,
//并對所有元素求和,然后將每個元素的指數除以總和,最終得到一個概率分布。這通常用于多分類問題,以輸出每個類別的概率
//激活函數
Softmax<TFloat32> softmax = tf.nn.softmax(add);
// 創建一個計算交叉熵的Mean對象
//損失函數
Mean<TFloat32> crossEntropy =
tf.math.mean( // 計算張量的平均值
tf.math.neg( // 計算張量的負值
tf.reduceSum( // 計算張量的和
tf.math.mul(labels, tf.math.log(softmax)), //計算標簽和softmax預測的對數乘積
tf.array(1) // 在指定軸上求和
)
),
tf.array(0) // 在指定軸上求平均值
);
// Back-propagate gradients to variables for training
//使用梯度下降優化器來最小化交叉熵損失函數。首先,創建了一個梯度下降優化器 optimizer,然后使用該優化器來最小化交叉熵損失函數 crossEntropy。
//梯度下降 https://www.cnblogs.com/guoyaohua/p/8542554.html
Optimizer optimizer = new GradientDescent(graph, LEARNING_RATE);
Op minimize = optimizer.minimize(crossEntropy);
// Compute the accuracy of the model
//使用 argMax 函數找出在給定軸上張量中最大值的索引,
Operand<TInt64> predicted = tf.math.argMax(softmax, tf.constant(1));
Operand<TInt64> expected = tf.math.argMax(labels, tf.constant(1));
//使用 equal 函數比較模型預測的標簽和實際標簽是否相等,再用 cast 函數將布爾值轉換為浮點數,最后使用 mean 函數計算準確率。
Operand<TFloat32> accuracy = tf.math.mean(tf.dtypes.cast(tf.math.equal(predicted, expected), TFloat32.class), tf.array(0));
// Run the graph
try (Session session = new Session(graph)) {
// Train the model
for (ImageBatch trainingBatch : dataset.trainingBatches(TRAINING_BATCH_SIZE)) {
try (TFloat32 batchImages = preprocessImages(trainingBatch.images());
TFloat32 batchLabels = preprocessLabels(trainingBatch.labels())) {
System.out.println("batchImages = " + batchImages.shape());
System.out.println("batchLabels = " + batchLabels.shape());
// 創建會話運行器
session.runner()
// 添加要最小化的目標
.addTarget(minimize)
// 通過feed方法將圖像數據輸入到模型中
.feed(images.asOutput(), batchImages)
// 通過feed方法將標簽數據輸入到模型中
.feed(labels.asOutput(), batchLabels)
// 運行會話
.run();
}
}
// Test the model
ImageBatch testBatch = dataset.testBatch();
try (TFloat32 testImages = preprocessImages(testBatch.images());
TFloat32 testLabels = preprocessLabels(testBatch.labels());
// 定義一個TFloat32類型的變量accuracyValue,用于存儲計算得到的準確率值
TFloat32 accuracyValue = (TFloat32) session.runner()
// 從會話中獲取準確率值
.fetch(accuracy)
.fetch(predicted)
.fetch(expected)
// 將images作為輸入,testImages作為數據進行喂養
.feed(images.asOutput(), testImages)
// 將labels作為輸入,testLabels作為數據進行喂養
.feed(labels.asOutput(), testLabels)
// 運行會話并獲取結果
.run()
// 獲取第一個結果并存儲在accuracyValue中
.get(0)) {
System.out.println("Accuracy: " + accuracyValue.getFloat());
}
// 保存模型
SavedModelBundle.Exporter exporter = SavedModelBundle.exporter("D:\\ai\\ai-demo").withSession(session);
Signature.Builder builder = Signature.builder();
builder.input("images", images);
builder.input("labels", labels);
builder.output("accuracy", accuracy);
builder.output("expected", expected);
builder.output("predicted", predicted);
Signature signature = builder.build();
SessionFunction sessionFunction = SessionFunction.create(signature, session);
exporter.withFunction(sessionFunction);
exporter.export();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static final int VALIDATION_SIZE = 5;
private static final int TRAINING_BATCH_SIZE = 100;
private static final float LEARNING_RATE = 0.2f;
private static TFloat32 preprocessImages(ByteNdArray rawImages) {
Ops tf = Ops.create();
// Flatten images in a single dimension and normalize their pixels as floats.
long imageSize = rawImages.get(0).shape().size();
return tf.math.div(
tf.reshape(
tf.dtypes.cast(tf.constant(rawImages), TFloat32.class),
tf.array(-1L, imageSize)
),
tf.constant(255.0f)
).asTensor();
}
private static TFloat32 preprocessLabels(ByteNdArray rawLabels) {
Ops tf = Ops.create();
// Map labels to one hot vectors where only the expected predictions as a value of 1.0
return tf.oneHot(
tf.constant(rawLabels),
tf.constant(MnistDataset.NUM_CLASSES),
tf.constant(1.0f),
tf.constant(0.0f)
).asTensor();
}
private final Graph graph;
private final MnistDataset dataset;
private SimpleMnist(Graph graph, MnistDataset dataset) {
this.graph = graph;
this.dataset = dataset;
}
public void loadModel(String exportDir) {
// load saved model
SavedModelBundle model = SavedModelBundle.load(exportDir, "serve");
try {
printSignature(model);
} catch (Exception e) {
throw new RuntimeException(e);
}
ByteNdArray validationImages = dataset.getValidationImages();
ByteNdArray validationLabels = dataset.getValidationLabels();
TFloat32 testImages = preprocessImages(validationImages);
System.out.println("testImages = " + testImages.shape());
TFloat32 testLabels = preprocessLabels(validationLabels);
System.out.println("testLabels = " + testLabels.shape());
Result run = model.session().runner()
.feed("Placeholder:0", testImages)
.feed("Placeholder_1:0", testLabels)
.fetch("ArgMax:0")
.fetch("ArgMax_1:0")
.fetch("Mean_1:0")
.run();
// 處理輸出
Optional<Tensor> tensor1 = run.get("ArgMax:0");
Optional<Tensor> tensor2 = run.get("ArgMax_1:0");
Optional<Tensor> tensor3 = run.get("Mean_1:0");
TInt64 predicted = (TInt64) tensor1.get();
Long predictedValue = predicted.getObject(0);
System.out.println("predictedValue = " + predictedValue);
TInt64 expected = (TInt64) tensor2.get();
Long expectedValue = expected.getObject(0);
System.out.println("expectedValue = " + expectedValue);
TFloat32 accuracy = (TFloat32) tensor3.get();
System.out.println("accuracy = " + accuracy.getFloat());
}
private static void printSignature(SavedModelBundle model) throws Exception {
MetaGraphDef m = model.metaGraphDef();
SignatureDef sig = m.getSignatureDefOrThrow("serving_default");
int numInputs = sig.getInputsCount();
int i = 1;
System.out.println("MODEL SIGNATURE");
System.out.println("Inputs:");
for (Map.Entry<String, TensorInfo> entry : sig.getInputsMap().entrySet()) {
TensorInfo t = entry.getValue();
System.out.printf(
"%d of %d: %-20s (Node name in graph: %-20s, type: %s)\n",
i++, numInputs, entry.getKey(), t.getName(), t.getDtype());
}
int numOutputs = sig.getOutputsCount();
i = 1;
System.out.println("Outputs:");
for (Map.Entry<String, TensorInfo> entry : sig.getOutputsMap().entrySet()) {
TensorInfo t = entry.getValue();
System.out.printf(
"%d of %d: %-20s (Node name in graph: %-20s, type: %s)\n",
i++, numOutputs, entry.getKey(), t.getName(), t.getDtype());
}
System.out.println("-----------------------------------------------");
}
}
六、待完善點
1、這里并沒有對提供web服務輸入圖片以及圖片數據二值話等進行處理。有興趣的小伙伴可以自己進行嘗試
2、并沒有使用卷積神經網絡等,只是用了wx+b和激活函數進行跳躍,以及階梯下降算法和交叉熵
3、沒有進行更多層級的設計等
今年相關BBC紀錄片播出后,舊杰尼斯事務所(現smile-up事務所,下文稱“杰尼斯”)的創始人Johnny喜多川生前性侵未成年的Jr(年輕的杰尼斯偶像練習生)的丑聞獲得廣泛關注。最終,時任杰尼斯社長喜多川的外甥女藤島景子承認喜多川性侵的事實并引咎辭職,杰尼斯也在繼任者東山紀之手上解體。12月8日,舊杰尼斯公司成立新經紀公司STARTO ENTERTAINMENT,由新公司負責管理原杰尼斯藝人,福田淳就任社長。
令人驚訝的是,早在上世紀60年代,喜多川就曾因為涉嫌猥褻杰尼斯事務所藝能學校的學員遭到起訴。而在上世紀80年代,一些元杰尼斯偶像練習生曾在自己的著作中提到了喜多川的性侵行為。1988年元フォーリーブス(四葉草組合)的成員北公次在《To 光GENJIへ》一書中公開揭發喜多川對其實施性侵。此后一段時間內,其他前杰尼斯偶像也陸續站出來揭發喜多川的行為。
1999年,著名媒體《周刊文春》報道了喜多川性侵事件,并且獲得多位被性侵的前杰尼斯偶像的證言,喜多川隨即以誹謗罪將《周刊文春》起訴至東京高等法院。2000年日本眾議院特別委員會討論了這一問題,但未能通過有效的相關政策。2004年該案件經過二審,日本東京高等法院最終裁定文春報道的部分內容為真(包括喜多川猥褻少年的行為),即使是喜多川也無法否認這些偶像的證言的真實性,但最終法院要求文春向喜多川賠償名譽損害費用,卻未對喜多川提出指控或進行判決。日本媒體對這一事件反應極為冷淡,主流媒體均未大肆報道此事[1]。
在此之后,直到喜多川2019年去世,公眾對此事都鮮有議論,杰尼斯事務所內的上層斗爭——藤島瑪麗和飯島三智、藤島景子和瀧澤秀明等人的斗爭吸引了公眾和粉絲的眼球。而SMAP、嵐、TOKIO等組合的大火,將杰尼斯事務所的聲望推到了新高,而之后的“偶像戰國”時期幾乎掩蓋了這位創始人的罪惡。喜多川去世后,原來以及現在的杰尼斯偶像們(從中居正廣到曾經在杰尼斯呆過的taka)均來到現場追悼,互聯網上哀悼的聲音占據了主流。直到2023年BBC的紀錄片,才讓人們重新發現了喜多川過去的惡行。
為什么多數媒體對喜多川的罪行不聞不問?除了文春等少數媒體一度對喜多川性侵事件進行深刻報道,并因此遭到起訴以外,各種日本媒體都選擇回避這個問題。筆者認為,這與日本特殊的事務所制度,以及事務所和媒體的關系息息相關,正是杰尼斯事務所和日本各類媒體公司錯綜復雜的關系、雙方的互利共生關系,以及事務所具有單向優勢的特點,導致媒體在喜多川丑聞的報道上長期失聲。
筆者8月在東京灣附近拍到的富士電視臺大樓
電視媒體在杰尼斯事件中的態度
電視臺是怎么經營的
在很長一段時間內,電視是日本人主要的娛樂方式。2015年NHK報告稱有94.5%的日本人在周中看電視,全國平均每人每天觀看電視3小時;哪怕在互聯網快速發展的2020年,依然有87.2%的人收看電視,每人每天看電視平均時長超過2.5小時[2]。因此,電視臺在喜多川事件中的回避態度,在一定程度上成為喜多川生前逃脫法律制裁的幫兇;如果電視臺選擇跟進報道喜多川的丑聞,可能會激發民眾對此事的關注和追蹤。那么,為什么電視臺會在喜多川事件中缺位?該問題的源頭自然是電視臺和杰尼斯之間的利益共同體關系。
日本主流電視臺包括一家國營電視臺(NHK)和五家民營電視臺(富士、TBS、東京臺、NTV和朝日),其他電視臺基本上都在這五大民營電視臺的電視的放送網絡中,因此民營電視臺是日本電視節目的主力軍。民營電視臺需要盈利來維持經營,主要盈利策略包括媒體業務和不動產業務(都市開發),其中媒體業務是營業額大頭,例如富士電視臺2023年第二和第三季度營業額中77.1%是媒體業務,其他各類業務加在一起僅占總營業額的22.9%,上一個半年的營業額比例與之相似[3]。足見媒體業務在富士為代表的民營電視臺營業額所占比例之高。
富士電視臺2023年第二三季度營業額和利潤報告
富士電視臺2022年第四季度和2023年第一季度營業額和利潤報告
對于民營電視臺,媒體業務又可以分為廣告業務、版權業務和配信收入業務等等,其中的收入大頭是廣告業務[4]。一般情況下,一家企業想要投放廣告,需要經過若干步驟:想投放廣告的贊助商將廣告費交給廣告代理商,廣告代理商聯系電視臺花錢投放,電視臺將廣告需求告知廣告制作公司并且付錢制作廣告,最后將廣告在自己電視臺上投放。在這一過程中,電視臺會抽走一定比例的廣告制作費用,成為電視臺的收入來源。
日本電視臺收入方式(左邊為NHK收入,右邊為民放電視臺收入)
廣告代理商和杰尼斯的關系
在廣告制作中,廣告代理公司和電視臺是關鍵的一環。日本的主要廣告代理公司是電通和博報堂,前者是最大的廣告代理公司,一定程度上把持著電視臺的經濟命脈,因此在日本非常具有影響力。例如著名女演員黑木瞳的丈夫是電通的副社長執行役員。2012年,她在青山學院中等學部上學的女兒被爆出聯合同學霸凌一位女同學,最后自己被停課,但被害者也遭到停課,學校也否認有霸凌的存在。外界傳言這件事和她父親是電通高層,以及捐錢給學校有關[5]。考慮到青山學院中等學部算是日本諸多名人子女所在的學校,不會輕易被某些家長施加的壓力所左右,可見在日本電通的地位不容小覷,并且對電視臺有極大的影響力。
盡管電通和杰尼斯的關系缺少細節材料,但是縱觀每年CM(廣告)的起用數量,我們會發現杰尼斯偶像所占的比例不低,至多可以占據男性藝人榜單前列的1/3,巔峰期的嵐幾乎可以做到全員上榜(5人)[6][7][8][9][10]。可以證實的是,嵐的成員,也是杰尼斯著名藝人櫻井翔的父親櫻井俊既是郵電部門的官僚,還在2017-2022年間擔任電通高層,一度官至代表取締役副社長的位置。雖然沒有直接證據證明櫻井俊在杰尼斯和電通之間的聯絡發揮了多大的作用,但我們有理由推測電通在背后對杰尼斯的事業起到了助推作用。
電視臺為什么選擇杰尼斯藝人
不管電通是否直接影響了杰尼斯藝人的熱度,但是從電通和電視臺的角度來看,如今的杰尼斯藝人確實比諸多藝術家更能幫助企業賺錢。21世紀的日本社會的特點之一是審美觀念的多元化。在上世紀70-80年代,某一電視節目收視率超過30%并不算一件稀奇事情,像NHK晨間劇《阿信》巔峰收視率甚至高達62%。到了上世紀90年代,日本電視劇收視率整體保持著高位,諸如《東京愛情故事》《同一屋檐下》《戀愛世紀》等經典日劇收視率能穩定在20%以上。
但進入21世紀,隨著個人電腦和互聯網的普及,日本電視劇的收視率開始逐漸下降。究其原因是在互聯網時代,人人都可以主動參與討論、分享個人觀點,尋找認同來強化自己的審美;這大大改變了電視時代,觀眾只能通過線下和紙質媒體分享自己觀點的情況。觀點交流的時效性大大加強,觀眾也從被動接受到主動創造。電視收視率開始逐步下降,想制作出一部國民級的電視劇難度越來越大。日本論壇上曾經有一個共識,認為民營電視臺電視劇收視率超過15%即可認為是實績(爆款電視劇),根據統計我們可以看到,隨著時間的推移,收視率超過15%的電視劇越來越少,到2021年后甚至已經不復存在[11]。另一方面,數據統計還顯示,東京臺以外四大民營電視臺各個節目(包括電視劇和綜藝)平均收視率都隨著年份從10%-14%下降到8%-12%上下[12]。可以說,“好的電視節目”不一定能帶來期待之中的受眾,而且相比于創造好的IP和好的劇本之難,選擇死忠粉絲足夠多的藝人來主演電視劇或者主持節目就容易多了。選擇粉絲多的藝人可以保證節目擁有以死忠粉為主的收視人數,死忠粉也愿意為了多看一眼偶像而看電視。因此,只要制作不要過于糟糕,這些節目的收視率下限仍然是有保障的。
2002-2023年電視劇實績數量
2003-2023年電視節目平均收視率
對于廣告代理商來說,他們對廣告代言人和廣告投放企業的選擇自有其根據。對于電視臺以及其投放時間的選擇,很大程度上受到電視臺收視率的影響——更高的收視率意味著更多的人在看電視,意味著可能有更多的潛在客戶,這會強化電視臺選擇藝人出演的邏輯;另一方面,廣告代理商本身也更傾向于選擇死忠粉絲多的藝人作為代言人,這也和廣告的邏輯息息相關。
廣告在電視上獲得成功,本質是因為電視在家庭空間內的親密感。電視消費的背景意味著公眾人物如家人一樣親密。電視公眾人物形象在數量和質量上創造親密感。電視通過特寫鏡頭提供更大的親密感,數字電視的清晰度和細節創造了一種逼真的表現,可以仔細閱讀每一個面部細節[13]。電視拉近了觀眾和明星之間的距離,私人和公共融合形成了一個完全由藝人定義的空間。簡言之,電視的親密感將藝人的體驗構建為一種媒介偷窺的形式。由于日本廣告時間很短,較長的廣告最多一分半,少的就幾十秒,為了在短時間中實現情感認同的重要性,廣告制作旨在將自發性和真實性的效果自然化[14]。
木村拓哉和廣瀨鈴出演麥當勞廣告,可見廣告對二人的面部特寫
在廣告中,藝人不僅受到關注,而且激起欲望。明星的作用是吸引觀眾的注意力,而廣告則依賴于明星的角色,通過渴望變得更接近或更像明星來塑造觀眾的行為。通過模仿和建模,表達了對更接近和形成聯系的渴望。通過識別,藝人的形象成為粉絲獲得快樂的來源。粉絲購買自己喜歡的藝人代言的商品本身就是表達對自己偶像的愛意、拉近和偶像距離,感受親密感的一部分[15]。因此,死忠粉更容易被激發購買的熱情,自然電視臺更喜歡死忠粉絲多的藝人,而非那些知名度和地位很高,但粉絲死忠度不夠、粉絲購買力不足的藝術家。
電視臺和廣告代理商對死忠粉絲多的藝人的強調完美符合杰尼斯偶像的特點。杰尼斯偶像以女性粉絲為主,他們最為吸引女粉絲的不是突出的實力,而是多變的形象。杰尼斯偶像的特點是缺乏深度,他們形象的生存狀態可以用“空洞”來形容。然而,這只會增強粉絲無休止地(重新)創造和消費關于這些圖標的個人敘事(或幻想)的傾向[16]。杰尼斯偶像具有某種真實性,并可能引發無盡的個人敘事,這取決于超越的空虛。在這里,“超越的空虛”意味著這些標志性人物的整個現實,而公眾/粉絲永遠無法獲得“現實”,這使得粉絲們下意識地被吸引。任何深度感都會阻止粉絲們制作出關于偶像的“易于消費的敘事”,復雜性與杰尼斯偶像的吸引力無關,杰尼斯偶像的空虛(或缺乏自我意識和原則)為自己的粉絲提供了某種美學和想象力的滿足。
除了形象上的空洞,杰尼斯大都以團隊出道,他們的活躍使得粉絲對偶像的想象圍繞成員在團隊內的角色和形象。也許從女粉絲的(潛意識)角度出發,去理解杰尼斯偶像的超然空虛的關鍵,是對杰尼斯偶像的集體定義。團體的團結,而非成員的個性構建了最初在潛意識上吸引了女性粉絲的情境。這些粉絲希望對他們最喜歡的偶像進行多樣化的敘述,因為每個偶像都是團體的一部分[17]。如今杰尼斯幾乎沒有直接以solo藝人出道的偶像,例如山下智久和龜梨和也等知名的個人偶像都曾或依舊有自己所屬的團隊(NEWS和KAT-TUN)。
杰尼斯偶像之所以能被想象,核心在于他們在外形上被視為shonen(男孩)。在日本的社會文化背景下,shonen投射出一種雌雄同體的感覺。理想化的shonen形象是杰尼斯偶像制作的核心,在女粉絲看來,shonen形象沒有太多的性內涵,因此女性能幻想/創造杰尼斯偶像的理想化shonen形象。這表明,她們試圖將偶像轉變為幻想,而不是接受他們真實的男性身份[18]。這也是為什么很多“杰姨”(杰尼斯的女粉絲)無法接受喜多川曾實施性侵的事實,因為這破壞了shonen的形象,將性加在一個可以親密想象的形象之上,因此她們會出于心理防御機制進行下意識的否定。
嵐
從杰尼斯演唱會的動員人數,不難發現杰尼斯偶像的死忠粉絲之多。日本巨蛋巡演(一次巡演中在五個巨蛋都召開演唱會)是歌手動員粉絲能力的體現,能開一次巨蛋巡演意味著死忠粉的數量驚人。歷史上共有29位日本和外國藝人開過巨蛋巡演,其中杰尼斯就占據了5席[19](嵐、SMAP、kinki kids、関ジャニ∞和Kis-My-Ft2)演唱會動員總人數超過1000萬人的藝人一共有6位,杰尼斯占據4位(嵐、SMAP、kinki kids和関ジャニ∞),演唱會動員總人數歷史第一是嵐[20]。從演唱會的動員能力,足見杰尼斯偶像的粉絲粘性,也難怪電視臺和電通會更青睞杰尼斯偶像。
座無虛席的SMAP北京演唱會
日媒估計的潛在收視率可以印證杰尼斯藝人個人收視率水平[21]。潛在收視率是指無論節目制作如何,因藝人出演而增加的收視率。杰尼斯藝人的潛在收視率在日本娛樂圈藝人中排名比較靠前,尤其是有三位潛在收視率高于10%的演員,僅僅落后于兩位國民男演員堺雅人和阿部寬。
根據日媒周刊現代披露的部分藝人潛在收視率數據統計做的前20排序
杰尼斯和日本事務所體制
實際上,日本娛樂圈本身就是事務所主導的體制。電視臺一類的媒體公司在接觸表演者方面對管理公司存在結構性依賴,對藝人外表和表現的決策權則完全掌握在事務所手中,因此媒體必須與管理公司協商訪問權限。事務所幾乎沒有給藝人控制自己職業方向的自由,同時藝人幾乎沒有議價權。一旦藝人和事務所鬧矛盾、嘗試退出事務所,事務所會直接封殺藝人,禁止他們上電視或者轉投其他事務所。
對于電視臺而言,五大電視網之間的競爭以及這些公司限制接觸藝人的能力意味著管理公司占據了上風:如果需求得不到滿足,他們可能會威脅并表示將給其他電視臺提供更好的待遇,所以電視臺“不得不”按照大事務所的要求封殺藝人[22]。而如果不用杰尼斯偶像,換其他事務所的藝人,則包含了巨大的風險。富士臺營業額和利潤關系是個很好的例子,盡管媒體部門營業額占總營業額比重高達77%,但是利潤卻只占總利潤的29%;即使在更好的情形下,媒體部門的利潤率也不如實體利潤率高。可見媒體部門盈利率低,稍有不慎就可能遭遇嚴重虧損。基于路徑依賴,電視臺會選擇比較穩妥地繼續和杰尼斯藝人合作,從而維持經營不虧損。至于立刻換掉杰尼斯藝人,那是無法想象的。在本次事件中,五大民營電視臺中只有實力相對弱的東京電視臺限制杰尼斯藝人出演東京臺的綜藝,其他四家電視臺紛紛選擇回避是否起用杰尼斯藝人的問題,或是維持當前起用藝人的方針。
杰尼斯對電視臺的支配不僅體現在封殺草剪剛、香取慎吾等退社的杰尼斯藝人,甚至吉田羊和天海佑希這樣的非杰尼斯演員也可能因為引起杰尼斯高層的不滿而被限制和杰尼斯偶像共演。在日本,這種對電視臺施壓來封殺藝人的舉措被稱為“(電視臺)忖度”。2023年9月7日,繼任杰尼斯社長的東山紀之在電視上間接承認了以前忖度的存在(而在2019年各家電視臺否認自己有過忖度),基本上坐實了杰尼斯對電視臺的單方面支配[23]。這也不難理解為什么各家電視臺為什么對喜多川的違法行徑關注甚少。
當地時間2023年10月6日,日本東京,因受前任社長喜多川的性丑聞的影響,杰尼斯事務所港區大樓的公司招牌拆除工作完成。
紙質媒體的共謀
紙質媒體和杰尼斯的利益關系
喜多川性侵丑聞最早是由紙質媒體,例如文春和其他雜志報道的。但是他們沒有堅持進行深入報道。在某些方面,紙媒和電視臺面臨同樣的局面,家業龐大的杰尼斯事務所開始對媒體的施壓。21世紀,紙質媒體整體呈現衰落趨勢,并沒有可以和杰尼斯抗衡的力量。除此之外,紙質媒體和電視臺一樣,并不完全是杰尼斯的敵人,相反還存在一定的共同利益。同時,這樣的局面還和日本人的觀念密切相關,即中產階級文化和戰敗文化。
在2016年AKB48的紀錄片《存在的理由》中,AKB48的工作人員專門去文春雜志的總部拜訪文春報道娛樂圈緋聞的負責人。盡管文春曾多次報道AKB48的緋聞,甚至因此造成AKB48的動蕩,但是從二者對話的態度,可見他們并不是想象的那樣水火不容。實際上,偶像和小報都有著相同的利益追求,那就是盡可能向社會曝光自己。這使得二者容易成為事實上的利益相關:小報記者會報道一些重大緋聞獲得曝光度,事務所有時候故意放出一些大新聞,有時候將無關痛癢的緋聞炒作,讓全社會認識他們自己的藝人。例如2012年指原莉乃被文春爆出入團之前談戀愛,這一期文春直接售罄。
而涉及到一些不利于創造共同利益的問題,這些雜志就可能會避而不談,2019年NGT48山口真帆被霸凌事件期間,文春的報道就蒼白無力,或顧左右而言他——這反過來證明雜志和事務所的利益相關性。就在杰尼斯新事務所STARTO ENTERTAINMENT成立當天,文春也專門采訪了新社長福田淳,這是福田淳接受的第一次采訪[24],可見文春和杰尼斯的關系并沒有人們想象中那樣差,雙方仍然需要彼此來賺取關注度。
AKB48工作人員采訪文春娛樂板塊負責人(背對鏡頭者)
此外,日本媒體實際上已經有小報化的趨勢。在酒井法子吸毒事件中,日本媒體的關注點似乎主要在酒井法子自己身上,通過產出既有共時性的,也有歷時性的報道,并將其具體化為一個道德故事,將結構性因素(酒井法子的違法行為或多或少是由她的名人/家庭生活方式引起的)和個人失敗混為一談。小報式的高度主觀、帶著偏見的過度報道還加強了社會對邊緣群體的偏見,從而規避了社會中存在的,卻因禁忌而不被討論的問題。酒井法子案引發了日本人對外國人犯罪的討論,加深對外國人在東京市中心販賣毒品的刻板印象,卻忽略了日本本地黑社會組織深入參與毒品交易,并將活動外包給外國人的現實[25]。另外,在最近報道寶冢宙組成員自殺的過程中,文春的報道也會像小報一樣編造故事。甚至喜多川事件中,文春之所以被判決賠償杰尼斯事務所,也是因為他們像小報一樣夸大其詞,被杰尼斯抓住把柄,盡管這樣的做法并未影響報道內容中的事實。
文春的行為反映出日本媒體雜志本身的小報化,其特點是從報道重要的社會問題轉向八卦的信息娛樂和名人瑣事。另外,也正是因為日本媒體對喜多川事件的回避,出現了沉默螺旋現象,希望進一步報道喜多川性侵丑聞的人因為看到被媒體放大的、漠不關心的聲音,認為自己是少數,因此保持沉默。例如日本著名作曲家服部良一的兒子在被喜多川性侵后因為找不到支持者而沉默[26],音樂制作人松尾潔因為不滿喜多川的行徑被杰尼斯停止合作,自己也長期保持沉默[27]。
這種轉變并非個例。趙鼎新曾經批評過西方主流媒體的保守性,經常跟著政府或所屬黨派轉變自己的報道風口,而小報則和這些日本媒體一樣側重于報道新奇和激烈的新聞。這是因為發達國家存在著能為精英階層以及大多數社會成員所認同的核心價值體系(即葛蘭西式的霸權性文化),這個體系本質就是中產階級價值體系[28],換言之,正是因為中產階級的體量足夠龐大,因此他們會更傾向于保守而不是對現實的激進改良。在市場的選擇中,那些保守且符合他們觀念的媒體,以及獵奇的小報留了下來。典型例子是以娛樂為主、面向中產階級的《泰晤士報》憑借著廣告收入降低自己的定價,最終壓垮了發行量小,廣告收入更少的左派報紙成為在英國大范圍發行的媒體。在日本,日共的雜志雖然還在發行,但是因為定價高而且觀點激進,導致其影響力遠不如文春一類的雜志,除了少數中老年日共支持者以外,大部分年輕人對其完全不感興趣。而日本人也對杰尼斯高層內斗興趣十足,對喜多川性侵這種血淋淋的事實則采取回避態度,社會問題可比富豪們的小故事要更難以接受。
筆者8月在日本拍到的代代木日共總部
另外,對于時尚雜志而言,杰尼斯和他們的利益相關性也很強。如杰尼斯藝人適合廣告一樣,杰尼斯藝人也因為大量的死忠粉,進而能夠提高雜志的銷量。出版業的相關人士指出,在雜志封面不起用杰尼斯藝人之后,雜志銷售額下降了近30%。所以各個時尚雜志沒有抵制杰尼斯的動機,哪怕在杰尼斯“忖度”的時候,也希望能及時結束抵制杰尼斯。一位出版業的相關人士迫切希望杰尼斯新公司能盡快行動起來,他說:“如果繼續這樣下去,就有可能停刊或廢刊了。”[29]
喜多川丑聞背后的戰敗文化
日本人對喜多川事件避而不談的現象背后,還存在著心理因素,即被害者認知。喜多川的肆無忌憚,正是因為其他人知道卻袖手旁觀甚至參與其中(例如東山紀之)。在如今對喜多川清算的時候,諸如木村拓哉、東山紀之、櫻井翔等在杰尼斯地位頗高的偶像,卻對此事保持沉默或者回避評論喜多川的錯誤。社會上對冷漠的旁觀者也有不少支持聲音,筆者認為這和他們認為自己也是受害者有關。正是因為他們意識到喜多川行為是錯誤的,而自己袖手旁觀也是不合理的,為了減輕自己心理上的壓力,這些人選擇將自己的形象轉換為被害者而非加害者(畢竟袖手旁觀實際上就是一種加害行為),從而減輕自己的心理壓力。
在探究戰后家庭應對黑歷史和制造戰后身份的策略時,德國心理學家引入了家庭相簿的概念,借指人們為家庭成員構建的正面形象,以此來防止負面家族歷史被暴露。在這種保護性的動態關系之下,子女和孫輩用填補信息空白來撫平創口,強調了家庭成員在戰爭中遭受的苦難,以及他們的勇氣和品德[30]。對于杰尼斯偶像和自認為有著親密關系(有家人感)的粉絲而言,這些負罪的杰尼斯偶像也是喜多川壓迫的受害者,是環境的受害者,是被迫袖手旁觀的——換言之,他們是脆弱、無助的人,除了那么做,別無選擇。
這種策略回避了偶像身上的道德責任,實際上是日本戰后戰敗文化的反映。日本在二戰中的戰敗使得他們發展出了自己獨特的戰敗文化,在這個文化體系中,對二戰的敘事提倡對失敗戰爭中的悲慘受害者表示同情和認同。在這種敘事中,“災難”的形象占了上風(一場規模空前的悲劇)強調了由殘酷軍事暴力所帶來的全部殘殺和破壞。這樣的文化側重于回避主要問題(為什么要打仗),僅僅一味地強調打仗不好,反而弱化了對軍國主義的批判性。戰敗文化反映在喜多川事件之中,是對喜多川進行籠統地批判,而不再考慮相關人物的責任,以及如何預防此類事件的發生。尤其是沒有形成道德責任感,沒法在需要決斷的時候承擔自己的道德責任。
總結
韋伯曾經提出“理性的牢籠”來描述科層制在社會的擴展,科層制的合理性使其存在于政府、公司、學校等各種機構中。另一方面,科層制也對個體進行異化,使之去人格化。正是在這種世界中,媒體失聲,人們面對杰尼斯這樣的高塔放棄抵抗,保持沉默,放任事件的發生。很明顯,不改變現在的事務所和媒體的共謀性,也不去深刻反思戰敗文化的不合理性,就很難對喜多川事件實現深刻的反思。
當然,互聯網的存在,搶占了電視的資源,為Kpop等不同于傳統日本本土審美的藝術風格提供了新的宣發渠道,從而沖擊了事務所的地位;同時因為其匿名性,給更多的人表達觀點的機會;女性主義通過互聯網的進入也動搖了日本社會父權制的基礎。綜合以上的種種改變,我們有理由希望互聯網在21世紀第三個十年給日本帶來更大的變化,而一切的出發點,便是不再保持沉默。
參考文獻
[1] 喜多川性侵案具體細節參考維基百科相關頁面:https://ja.wikipedia.org/wiki/ジャニー喜多川による性加害問題
[2] 日本人觀看電視的數據見NHK生活事件調查:https://www.nhk.or.jp/bunken/yoron-jikan/,
[3] 富士電視臺的營業額和利潤數據參見富士電視臺自己的投資情報中間報告書:https://www.fujimediahd.co.jp/ir/l_report.html
[4] テレビ業界は赤字?ビジネスモデルや民放キー局今後の収益構造を解説https://matcher.jp/dictionary/articles/381
[5] 黒木瞳の娘の寶塚不合格…落ちる理由に驚愕https://www.xn--u9jy52gkffn9q8qbux6ab4xi9c4wsx57a.com/kurokihitomi-daughter-takarazuka
[6] 2022年TV-CMタレントランキングを発表https://mdata.tv/info/20221208_01/
[7] 2021年TV-CMタレントランキングを発表 https://mdata.tv/info/20211207_01/
[8] 2020年TV-CMタレントランキングを発表https://mdata.tv/info/20201204_01/
[9] 2019年TV-CMタレントランキングを発表https://mdata.tv/info/20191203_01/
[10] 2018年TV-CMタレントランキングを発表https://mdata.tv/info/20181204_01/
[11] 歴代ドラマ視聴率情報https://doraman.net/sp/index_rank/best30
[12] 各局とも下落続く…主要テレビ局の複數年にわたる視聴率推移(2023年12月公開版) https://news.yahoo.co.jp/expert/articles/25a5ae4a96219047e2f06446351e4a129c0437c9
[13] Patrick W. Galbraith, Jason G. Karlin.2012.Idols and Celebrity in Japanese Media Culture. New York: Palgrave Macmillan:9
[14] Patrick W. Galbraith, Jason G. Karlin.2012.Idols and Celebrity in Japanese Media Culture. New York: Palgrave Macmillan:77
[15] Patrick W. Galbraith, Jason G. Karlin.2012.Idols and Celebrity in Japanese Media Culture. New York: Palgrave Macmillan:79
[16] Patrick W. Galbraith, Jason G. Karlin.2012.Idols and Celebrity in Japanese Media Culture. New York: Palgrave Macmillan:99
[17] Patrick W. Galbraith, Jason G. Karlin.2012.Idols and Celebrity in Japanese Media Culture. New York: Palgrave Macmillan:102
[18] Patrick W. Galbraith, Jason G. Karlin.2012.Idols and Celebrity in Japanese Media Culture. New York: Palgrave Macmillan:104
[19] 簡單科普190822為止,哪些藝人開過蛋巡?https://www.douban.com/group/topic/150290015/?_i=22176580NMbohA
[20] 部分歌手演唱會動員人數排名 https://www.douban.com/group/topic/295864875/?_i=22176060NMbohA
[21] 橋本環奈8.5%、阿部寛10.8%…極秘情報「潛在視聴率」を獨占入手!ジャニーズ崩壊後、確実に「數字」を取れる「俳優の名前」 https://gendai.media/articles/-/117759?imp=0
[22] Patrick W. Galbraith, Jason G. Karlin.2012.Idols and Celebrity in Japanese Media Culture. New York: Palgrave Macmillan:44
[23] 東山 メディアの忖度「必要ない」 退所タレントの活動は妨害しない https://www.sponichi.co.jp/entertainment/news/2023/09/08/kiji/20230908s00041000154000c.html
[24] 舊ジャニーズ「STARTO ENTERTAINMENT」福田淳新社長が真っ先に“週刊文春”の取材を受けた理由「性加害問題は…」〈獨占告白150分〉https://bunshun.jp/articles/-/67542
[25] Patrick W. Galbraith, Jason G. Karlin.2012.Idols and Celebrity in Japanese Media Culture. New York: Palgrave Macmillan:63
[26] 服部良一氏の78歳次男「ジャニー喜多川氏から幼少期に性被害受けた」…都內で會見 https://www.yomiuri.co.jp/culture/20230715-OYT1T50223/
[27] 松尾潔氏、山下達郎から離れるファンに呼びかけ「お考えを改める旨を表明したら…」https://www.nikkansports.com/entertainment/news/202308300000184.html
[28] 趙鼎新:《社會與政治運動講義》,2012,北京:社會科學文獻出版社.
[29] STARTO社設立に安堵する出版業界 非ジャニーズ表紙で「売り上げ落ちた」 https://www.tokyo-sports.co.jp/articles/-/285412
[30] 橋本明子:《漫長的戰敗:日本的文化創傷、記憶與認同》,李鵬程譯,2021,上海:上海三聯書店
*請認真填寫需求信息,我們會在24小時內與您取得聯系。