Source browser

examples/lenet_checks.hpp

Read the complete file below or download its original bytes. Line numbers provide stable links to a declaration or processing step.

File fingerprint
1/** @file lenet_checks.hpp
2 * @brief Independent double-precision checks for the complete LeNet-style model.
3 * The reference uses ordinary host arrays and explicit layer calculations.
4 * It checks every intermediate feature and final logit, including edge padding,
5 * channel ordering, flattening, pooling and the two independent batch images.
6 */
7#ifndef SHDR_EXAMPLES_LENET_CHECKS_HPP // Include this independent model check only once.
8#define SHDR_EXAMPLES_LENET_CHECKS_HPP // Remember that the model check is available.
9#include "runner.hpp" // Select CPU or a supported graphics execution profile.
10#include "lenet_support.hpp" // Use the complete seven-stage native classifier.
11#include <random> // Build repeatable independent model and input fixtures.
12namespace shdr_examples { // Keep the model checks with the reusable examples.
13namespace lenet_detail { // Keep reference calculations separate from deployed shader code.
14struct reference_result { // Retain every independently calculated model layer.
15    std::vector<double> conv1, pool1, conv2, pool2, hidden1, hidden2, // Keep spatial features, dense features and final scores in double precision.
16        logits; // Keep spatial features, dense features and final scores in double precision.
17}; // Finish the reference feature record.
18inline std::vector<double> // Compute a padded learned convolution in ordinary double precision.
19convolution(const std::vector<double>& input, // Compute a padded learned convolution in ordinary double precision.
20            const std::vector<double>& weights, // Read the flattened model in double precision.
21            unsigned batch, // Process this many independent images.
22            unsigned width, // Specify the width of each input feature plane.
23            unsigned sources, // Read this many source channels or vector features.
24            unsigned outputs, // Produce this many channels or vector features.
25            unsigned padding, // Specify the number of zero pixels around the input.
26            unsigned weight_offset, // Locate the first weight of this layer.
27            unsigned bias_offset) { // Compute a padded learned convolution in ordinary double precision.
28    const unsigned side = // Find the output side for a five-by-five valid kernel.
29        width + 2u * padding - 4u; // Find the output side for a five-by-five valid kernel.
30    std::vector<double> result(batch * outputs * side * // Allocate each complete output channel plane.
31                               side); // Allocate each complete output channel plane.
32    for (unsigned image = 0; image < batch; // Keep independent batch images in their original order.
33         ++image) { // Keep independent batch images in their original order.
34        for (unsigned target = 0; target < outputs; ++target) { // Visit each learned output filter.
35            for (unsigned y = 0; y < side; ++y) { // Visit each output row.
36                for (unsigned x = 0; x < side; ++x) { // Visit each output column.
37                    double value = weights[bias_offset + // Start with the output filter's learned bias.
38                                           target]; // Start with the output filter's learned bias.
39                    for (unsigned source = 0; source < sources; // Sum all source feature-plane contributions.
40                         ++source) { // Sum all source feature-plane contributions.
41                        for (unsigned ky = 0; ky < 5u; ++ky) { // Visit the five kernel rows.
42                            for (unsigned kx = 0; kx < 5u; ++kx) { // Visit the five kernel columns.
43                                const int sx = // Find the corresponding unpadded input column.
44                                    static_cast<int>(x + kx) - // Find the corresponding unpadded input column.
45                                    static_cast<int>( // Find the corresponding unpadded input column.
46                                        padding); // Find the corresponding unpadded input column.
47                                const int sy = // Find the corresponding unpadded input row.
48                                    static_cast<int>(y + ky) - // Find the corresponding unpadded input row.
49                                    static_cast<int>( // Find the corresponding unpadded input row.
50                                        padding); // Find the corresponding unpadded input row.
51                                if (sx < 0 || sy < 0 || sx >= static_cast<int>(width) || // Treat all pixels outside the image as zero.
52                                    sy >= static_cast<int>(width)) // Treat all pixels outside the image as zero.
53                                    continue; // Treat all pixels outside the image as zero.
54                                const unsigned position = // Select the matching NCHW feature value.
55                                    (image * sources + source) * width * width + // Select the matching NCHW feature value.
56                                    unsigned(sy) * width + // Select the matching NCHW feature value.
57                                    unsigned(sx); // Select the matching NCHW feature value.
58                                const unsigned coefficient = // Select the independently indexed learned coefficient.
59                                    weight_offset + ((target * sources + source) * 5u + ky) * 5u + // Select the independently indexed learned coefficient.
60                                    kx; // Select the independently indexed learned coefficient.
61                                value += // Accumulate this contribution in double precision.
62                                    weights[coefficient] * // Accumulate this contribution in double precision.
63                                    input // Accumulate this contribution in double precision.
64                                        [position]; // Accumulate this contribution in double precision.
65                            } // Finish this kernel row.
66                        } // Finish this source kernel.
67                    } // Finish the sum over source channels.
68                    result[(image * outputs + target) * side * side + y * side + x] = // Apply ReLU after the complete convolution sum.
69                        std::max(0.0, value); // Apply ReLU after the complete convolution sum.
70                } // Finish this output row.
71            } // Finish this output channel.
72        } // Finish this image's learned feature planes.
73    } // Finish all independent batch images.
74    return result; // Return the independently calculated convolution layer.
75} // Finish the convolution reference.
76inline std::vector<double> // Downsample channel planes with an explicit double average.
77pooling(const std::vector<double>& input, // Downsample channel planes with an explicit double average.
78        unsigned batch, // Process this many independent images.
79        unsigned channels, // Pool each feature channel independently.
80        unsigned width) { // Downsample channel planes with an explicit double average.
81    const unsigned side = width / 2u; // Halve the even spatial dimension.
82    std::vector<double> result(batch * channels * side * // Allocate each complete downsampled plane.
83                               side); // Allocate each complete downsampled plane.
84    for (unsigned plane = 0; plane < batch * channels; // Keep batch and channel planes independent.
85         ++plane) { // Keep batch and channel planes independent.
86        for (unsigned y = 0; y < side; ++y) { // Visit each pooled output row.
87            for (unsigned x = 0; x < side; ++x) { // Visit each pooled output column.
88                double sum = 0.0; // Begin the four-pixel double accumulator at zero.
89                for (unsigned dy = 0; dy < 2u; ++dy) { // Visit the upper and lower source rows.
90                    for (unsigned dx = 0; dx < 2u; // Visit the left and right source columns.
91                         ++dx) { // Visit the left and right source columns.
92                        sum += input[plane * width * width + (2u * y + dy) * width + 2u * x + // Add this source pixel to the explicit double sum.
93                                     dx]; // Add this source pixel to the explicit double sum.
94                    } // Finish this source square row.
95                } // Finish this complete two-by-two source square.
96                result[plane * side * side + y * side + x] = // Store the average in the same plane ordering.
97                    sum / 4.0; // Store the average in the same plane ordering.
98            } // Finish this pooled output row.
99        } // Finish this pooled output plane.
100    } // Finish the independent image and channel planes.
101    return result; // Return the independently downsampled feature layer.
102} // Finish the average-pooling reference.
103inline std::vector<double> // Compute an ordinary double matrix-vector product for every image.
104dense(const std::vector<double>& input, // Compute an ordinary double matrix-vector product for every image.
105      const std::vector<double>& weights, // Read the flattened model in double precision.
106      unsigned batch, // Process this many independent images.
107      unsigned sources, // Read this many source channels or vector features.
108      unsigned outputs, // Produce this many channels or vector features.
109      unsigned weight_offset, // Locate the first weight of this layer.
110      unsigned bias_offset, // Locate the first bias of this layer.
111      bool relu) { // Compute an ordinary double matrix-vector product for every image.
112    std::vector<double> result(batch * // Allocate a contiguous output vector for each image.
113                               outputs); // Allocate a contiguous output vector for each image.
114    for (unsigned image = 0; image < batch; // Process the independent flattened input vectors.
115         ++image) { // Process the independent flattened input vectors.
116        for (unsigned target = 0; target < outputs; // Visit each learned output coordinate.
117             ++target) { // Visit each learned output coordinate.
118            double sum = // Start the dot product with the learned bias.
119                weights[bias_offset + target]; // Start the dot product with the learned bias.
120            for (unsigned source = 0; source < sources; // Visit every contributing input feature.
121                 ++source) { // Visit every contributing input feature.
122                sum += // Accumulate the row-major learned projection.
123                    weights[weight_offset + target * sources + source] * // Accumulate the row-major learned projection.
124                    input[image * sources + source]; // Accumulate the row-major learned projection.
125            } // Finish the input-feature sum.
126            result[image * outputs + target] = // Keep logits linear and hidden features nonnegative.
127                relu ? std::max(0.0, sum) // Keep logits linear and hidden features nonnegative.
128                     : sum; // Keep logits linear and hidden features nonnegative.
129        } // Finish the image's output vector.
130    } // Finish all independent image vectors.
131    return result; // Return the independently calculated dense layer.
132} // Finish the dense reference.
133inline void // Compare every native feature with its independent double result.
134compare(const shdr::buffer<float>& actual, // Compare every native feature with its independent double result.
135        const std::vector<double>& expected, // Use the independently computed double-precision features.
136        const char* layer) { // Compare every native feature with its independent double result.
137    if (actual.size() != expected.size()) // Require matching layer dimensions before comparing values.
138        throw shdr::error( // Reject an inconsistent model shape before reading output.
139            "LeNet reference layer size mismatch"); // Reject an inconsistent model shape before reading output.
140    const float* values = // Obtain the completed native output values.
141        actual.data(); // Synchronize the layer only for this untimed correctness check.
142    for (unsigned i = 0; i < expected.size(); // Verify every image, channel and feature coordinate.
143         ++i) { // Verify every image, channel and feature coordinate.
144        const float reference = static_cast<float>( // Convert the double oracle value for a float comparison.
145            expected[i]); // Convert the double oracle value for a float comparison.
146        require_close( // Check this value against the independent layer oracle.
147            values[i], // Use the native CPU or GPU feature value.
148            reference, // Use the oracle feature rounded to storage precision.
149            layer, // Identify the layer if this comparison fails.
150            0.00002f + // Allow a small absolute error around zero.
151                0.00003f * // Scale the tolerance with the expected feature magnitude.
152                    std::fabs( // Measure the absolute expected magnitude.
153                        reference)); // Allow float accumulation error while retaining a strict layer check.
154    } // Finish all feature comparisons.
155} // Finish the model-layer comparison.
156} // namespace lenet_detail
157/** @brief Calculate every model layer independently using a double host reference. */
158inline lenet_detail::reference_result lenet_reference( // Accept the same complete input and parameter layout as the native classifier.
159    const std::vector<float>& inputs, // Accept the complete batch of normalized input images.
160    const std::vector<float>& parameters, // Accept the documented flattened model record.
161    unsigned // Accept the number of independent images in this reference batch.
162        batch) { // Accept the same complete input and parameter layout as the native classifier.
163    if (!batch || batch > 512u || inputs.size() != batch * 784u || parameters.size() != 61706u) // Require the exact batch, image and model geometry.
164        throw shdr::error( // Require complete model and batch records.
165            "invalid LeNet reference geometry"); // Require complete model and batch records.
166    const std::vector<double> image(inputs.begin(), inputs.end()), // Convert each input value to the oracle's double precision.
167        weights(parameters.begin(), // Convert each stored parameter to double precision.
168                parameters.end()); // Convert the float parameters to the independent double reference.
169    lenet_detail::reference_result result; // Keep every independently computed intermediate layer.
170    result.conv1 = // Compute the independent first convolution features.
171        lenet_detail::convolution(image, // Use explicit double-precision convolution loops.
172                                  weights, // Read the independent double-precision model record.
173                                  batch, // Process every image in this oracle call.
174                                  28u, // Use the 28-pixel input dimension.
175                                  1u, // Read one grayscale source plane.
176                                  6u, // Use the six first-block feature planes.
177                                  2u, // Pad each edge with two implicit zero pixels.
178                                  0u, // Start the first convolution weight record.
179                                  150u); // Reproduce padded first convolution in double precision.
180    result.pool1 = // Compute the independent first pooled features.
181        lenet_detail::pooling(result.conv1, batch, 6u, 28u); // Reproduce first average pooling.
182    result.conv2 = lenet_detail::convolution( // Compute the full six-to-sixteen second convolution.
183        result.pool1, // Read the first independent pooled feature record.
184        weights, // Read the independent double-precision model record.
185        batch, // Process every image in this oracle call.
186        14u, // Use the 14-pixel first pooled dimension.
187        6u, // Use the six first-block feature planes.
188        16u, // Use the sixteen second-block feature planes.
189        0u, // Apply the second convolution over valid five-by-five input windows.
190        156u, // Start the second convolution weight record.
191        2556u); // Reproduce full C3 connectivity across six source planes.
192    result.pool2 = lenet_detail::pooling( // Compute the independent second average pooling.
193        result.conv2, batch, 16u, 10u); // Reproduce second average pooling in NCHW order.
194    result.hidden1 = lenet_detail::dense(result.pool2, // Project the independent pooled features to 120 values.
195                                         weights, // Read the independent double-precision model record.
196                                         batch, // Process every image in this oracle call.
197                                         400u, // Read all sixteen five-by-five pooled planes.
198                                         120u, // Use the 120-feature first dense representation.
199                                         2572u, // Start the 400-to-120 dense weight record.
200                                         50572u, // Start the 120 first dense biases.
201                                         true); // Reproduce the flattened full C5 projection.
202    result.hidden2 = lenet_detail::dense(result.hidden1, // Project the independent hidden features to 84 values.
203                                         weights, // Read the independent double-precision model record.
204                                         batch, // Process every image in this oracle call.
205                                         120u, // Use the 120-feature first dense representation.
206                                         84u, // Use the 84-feature second dense representation.
207                                         50692u, // Start the 120-to-84 dense weight record.
208                                         60772u, // Start the 84 second dense biases.
209                                         true); // Reproduce the F6 feature projection.
210    result.logits = lenet_detail::dense(result.hidden2, // Compute the ten independent linear class logits.
211                                        weights, // Read the independent double-precision model record.
212                                        batch, // Process every image in this oracle call.
213                                        84u, // Use the 84-feature second dense representation.
214                                        10u, // Use the ten-value dimension required by this layer.
215                                        60856u, // Start the 84-to-10 classifier weight record.
216                                        61696u, // Start the ten final classifier biases.
217                                        false); // Reproduce the learned linear ten-class output.
218    return result; // Return all reference layers for detailed validation.
219} // Finish complete independent model inference.
220/** @brief Verify all seven native layers on two distinct seeded and impulse images. */
221inline void check_lenet_classifier( // Verify the complete model on the chosen execution backend.
222    runner& run) { // Check geometry and every model layer against independent calculations.
223    lenet_classifier model(2); // Allocate two independent complete classifiers in one batch.
224    std::mt19937 generator(1978u); // Reproduce the same synthetic model on every backend.
225    std::uniform_real_distribution<float> weights(-0.08f, 0.08f), // Keep model values bounded and input pixels in the documented range.
226        pixels(0.0f, 1.0f); // Keep model values bounded and input pixels in the documented range.
227    std::vector<float> parameters( // Allocate one complete deterministic test model.
228        model.parameter_count); // Hold every learned value in its published layout.
229    for (float& value : parameters) // Initialize every learned model value.
230        value = weights( // Draw a reproducible small signed parameter.
231            generator); // Give all convolution and dense coefficients nontrivial seeded values.
232    for (unsigned i = 0; i < 6u; ++i) // Initialize each first convolution bias.
233        parameters[150u + i] = 0.01f + 0.003f * i; // Keep distinct first-layer biases visible.
234    for (unsigned i = 0; i < 16u; ++i) // Initialize each second convolution bias.
235        parameters[2556u + i] = // Keep second-block activations available for later checks.
236            0.02f + 0.005f * (i % 3u); // Keep second-layer channels distinguishable.
237    for (unsigned i = 0; i < 120u; ++i) // Initialize every first dense bias.
238        parameters[50572u + i] = // Keep first dense activations available for later checks.
239            0.02f + 0.002f * (i % 7u); // Exercise nonzero hidden features after flattening.
240    for (unsigned i = 0; i < 84u; ++i) // Initialize every second dense bias.
241        parameters[60772u + i] = // Keep second dense activations available for later checks.
242            0.01f + 0.003f * (i % 5u); // Exercise the learned 84-feature projection.
243    for (unsigned i = 0; i < 10u; ++i) // Initialize all final digit-score biases.
244        parameters[61696u + i] = // Give each class a distinct deterministic bias.
245            -0.02f + 0.004f * i; // Keep the final linear class scores distinct.
246    std::vector<float> inputs(2u * 784u, // Allocate two initially zero test images.
247                              0.0f); // Keep the second image zero except for its explicit impulses.
248    for (unsigned i = 0; i < 784u; ++i) // Fill the first image with reproducible grayscale data.
249        inputs[i] = pixels(generator); // Use a seeded grayscale image in the first batch slot.
250    inputs[784u] = 1.0f; // Put an impulse at the second image's upper-left edge.
251    inputs[784u + 14u * 28u + 12u] = 0.75f; // Put a distinct interior impulse in the second image.
252    inputs[2u * 784u - 1u] = 0.4f; // Put a third impulse at the lower-right edge.
253    model.load_parameters( // Load the independent deterministic model into native storage.
254        parameters); // Copy the complete synthetic model into shared CPU and GPU storage.
255    std::copy( // Copy both test images into the model input resource.
256        inputs.begin(), // Start at the first normalized test image.
257        inputs.end(), // Finish at the end of the second test image.
258        model.inputs.data()); // Copy both independent input images in their original order.
259    const lenet_detail::reference_result expected = // Compute every layer with a separate double-precision oracle.
260        lenet_reference(inputs, // Run the independent complete classifier calculation.
261                        parameters, // Use the same stored model values for the oracle.
262                        2u); // Compute the full independent double oracle before native execution.
263    run.run( // Execute all seven linked native model stages.
264        model // Use the classifier's complete inference pipeline.
265            .inference_pipeline()); // Execute the same seven linked native stages on the selected backend.
266    lenet_detail::compare( // Compare every feature of the next native layer.
267        model.conv1, // Read the first native convolution feature record.
268        expected.conv1, // Use the independent first convolution features.
269        "LeNet padded first convolution"); // Check every padded border and first-layer feature.
270    lenet_detail::compare( // Compare every feature of the next native layer.
271        model.pool1, // Read the first native pooled feature record.
272        expected.pool1, // Use the independent first pooling features.
273        "LeNet first average pooling"); // Check the first downsampled feature planes.
274    lenet_detail::compare( // Compare every feature of the next native layer.
275        model.conv2, // Read the second native convolution feature record.
276        expected.conv2, // Use the independent second convolution features.
277        "LeNet full C3 convolution"); // Check every learned cross-channel contribution.
278    lenet_detail::compare( // Compare every feature of the next native layer.
279        model.pool2, // Read the second native pooled feature record.
280        expected.pool2, // Use the independent second pooling features.
281        "LeNet second average pooling"); // Check the NCHW features feeding the dense classifier.
282    lenet_detail::compare( // Compare every feature of the next native layer.
283        model.hidden1, // Read the native 120-feature hidden representation.
284        expected.hidden1, // Use the independent 120-feature hidden representation.
285        "LeNet full C5 projection"); // Check flattening and the 120-feature projection.
286    lenet_detail::compare( // Compare every feature of the next native layer.
287        model.hidden2, expected.hidden2, "LeNet F6 projection"); // Check the 84-feature ReLU stage.
288    lenet_detail::compare(model.logits, // Compare all native linear digit scores.
289                          expected.logits, // Use the independently computed linear digit scores.
290                          "LeNet ten-class logits"); // Check every final linear class score.
291    std::cout // Print a concise confirmation of complete model verification.
292        << "LeNet-style classifier: all seven layers and 61706 parameters verified against a double oracle\n"; // Report the complete independent inference check.
293} // Finish the native classifier validation.
294} // namespace shdr_examples
295#endif // Finish the model-check include guard.