
import sys
import os.path
import cv2

from big_ol_pile_of_manim_imports import *

from nn.network import *
from nn.part1 import *

POSITIVE_COLOR = BLUE
NEGATIVE_COLOR = RED

def get_training_image_group(train_in, train_out):
    image = MNistMobject(train_in)
    image.set_height(1)
    arrow = Vector(RIGHT, color = BLUE, buff = 0)
    output = np.argmax(train_out)
    output_tex = TexMobject(str(output)).scale(1.5)
    result = Group(image, arrow, output_tex)
    result.arrange(RIGHT)
    result.to_edge(UP)
    return result

def get_decimal_vector(nums, with_dots = True):
    decimals = VGroup()
    for num in nums:
        decimal = DecimalNumber(num)
        if num > 0:
            decimal.set_color(POSITIVE_COLOR)
        else:
            decimal.set_color(NEGATIVE_COLOR)
        decimals.add(decimal)
    contents = VGroup(*decimals)
    if with_dots:
        dots = TexMobject("\\vdots")
        contents.submobjects.insert(len(decimals)/2, dots)
    contents.arrange(DOWN)
    lb, rb = brackets = TexMobject("\\big[", "\\big]")
    brackets.scale(2)
    brackets.stretch_to_fit_height(1.2*contents.get_height())
    lb.next_to(contents, LEFT, SMALL_BUFF)
    rb.next_to(contents, RIGHT, SMALL_BUFF)

    result = VGroup(lb, contents, brackets)
    result.lb = lb
    result.rb = rb
    result.brackets = brackets
    result.decimals = decimals
    result.contents = contents
    if with_dots:
        result.dots = dots
    return result


########

class ShowLastVideo(TeacherStudentsScene):
    def construct(self):
        frame = ScreenRectangle()
        frame.set_height(4.5)
        frame.to_corner(UP+LEFT)
        title = TextMobject("But what \\emph{is} a Neural Network")
        title.move_to(frame)
        title.to_edge(UP)
        frame.next_to(title, DOWN)

        assumption_words = TextMobject(
            "I assume you've\\\\ watched this"
        )
        assumption_words.move_to(frame)
        assumption_words.to_edge(RIGHT)
        arrow = Arrow(RIGHT, LEFT, color = BLUE)
        arrow.next_to(assumption_words, LEFT)


        self.play(
            ShowCreation(frame),
            self.teacher.change, "raise_right_hand"
        )
        self.play(
            Write(title),
            self.get_student_changes(*["thinking"]*3)
        )
        self.play(
            Animation(title),
            GrowArrow(arrow),
            FadeIn(assumption_words)
        )
        self.wait(5)

class ShowPlan(Scene):
    def construct(self):
        title = TextMobject("Plan").scale(1.5)
        title.to_edge(UP)
        h_line = Line(LEFT, RIGHT).scale(FRAME_X_RADIUS)
        h_line.set_color(WHITE)
        h_line.next_to(title, DOWN)
        self.add(title, h_line)

        items = VGroup(*[
            TextMobject("$\\cdot$ %s"%s)
            for s in [
                "Recap",
                "Gradient descent",
                "Analyze this network",
                "Where to learn more",
                "Research corner",
            ]
        ])
        items.arrange(DOWN, buff = MED_LARGE_BUFF, aligned_edge = LEFT)
        items.to_edge(LEFT, buff = LARGE_BUFF)

        rect = SurroundingRectangle(VGroup(*items[1:3]))

        self.add(items)
        self.wait()
        self.play(ShowCreation(rect))
        self.play(FadeOut(rect))
        for item in items[1:]:
            to_fade = VGroup(*[i for i in items if i is not item])
            self.play(
                to_fade.set_fill, None, 0.5,
                item.set_fill, None, 1,
            )
            self.wait()

class BeginAndEndRecap(Scene):
    def construct(self):
        recap = TexMobject(
            "\\langle", "\\text{Recap}", "\\rangle"
        )
        new_langle = TexMobject("\\langle/")
        new_langle.scale(2)
        recap.scale(2)
        new_langle.move_to(recap[0], RIGHT)

        self.add(recap)
        self.wait(2)
        self.play(Transform(recap[0], new_langle))
        self.wait(2)

class PreviewLearning(NetworkScene):
    CONFIG = {
        "layer_sizes" : DEFAULT_LAYER_SIZES,
        "network_mob_config" : {
            "neuron_to_neuron_buff" : SMALL_BUFF,
            "layer_to_layer_buff" : 2,
            "edge_stroke_width" : 1,
            "neuron_stroke_color" : WHITE,
            "neuron_stroke_width" : 2,
            "neuron_fill_color" : WHITE,
            "average_shown_activation_of_large_layer" : False,
            "edge_propogation_color" : GREEN,
            "edge_propogation_time" : 2,
            "include_output_labels" : True,
        },
        "n_examples" : 15,
        "max_stroke_width" : 3,
        "stroke_width_exp" : 3,
        "eta" : 3.0,
        "positive_edge_color" : BLUE,
        "negative_edge_color" : RED,
        "positive_change_color" : BLUE_C,
        "negative_change_color" : average_color(*2*[RED] + [YELLOW]),
        "default_activate_run_time" : 1.5,
    }
    def construct(self):
        self.initialize_network()
        self.add_training_words()
        self.show_training()

    def initialize_network(self):
        self.network_mob.scale(0.7)
        self.network_mob.to_edge(DOWN)
        self.color_network_edges()

    def add_training_words(self):
        words = TextMobject("Training in \\\\ progress$\\dots$")
        words.scale(1.5)
        words.to_corner(UP+LEFT)

        self.add(words)

    def show_training(self):
        training_data, validation_data, test_data = load_data_wrapper()
        for train_in, train_out in training_data[:self.n_examples]:
            image = get_training_image_group(train_in, train_out)
            self.activate_network(train_in, FadeIn(image))
            self.backprop_one_example(
                train_in, train_out, 
                FadeOut(image), self.network_mob.layers.restore
            )

    def activate_network(self, train_in, *added_anims, **kwargs):
        network_mob = self.network_mob
        layers = network_mob.layers
        layers.save_state()
        activations = self.network.get_activation_of_all_layers(train_in)
        active_layers = [
            self.network_mob.get_active_layer(i, vect)
            for i, vect in enumerate(activations)
        ]
        all_edges = VGroup(*it.chain(*network_mob.edge_groups))
        run_time = kwargs.get("run_time", self.default_activate_run_time)
        edge_animation = LaggedStartMap(
            ShowCreationThenDestruction, 
            all_edges.copy().set_fill(YELLOW),
            run_time = run_time,
            lag_ratio = 0.3,
            remover = True,
        )
        layer_animation = Transform(
            VGroup(*layers), VGroup(*active_layers),
            run_time = run_time,
            lag_ratio = 0.5,
            rate_func=linear,
        )

        self.play(edge_animation, layer_animation, *added_anims)

    def backprop_one_example(self, train_in, train_out, *added_outro_anims):
        network_mob = self.network_mob
        nabla_b, nabla_w = self.network.backprop(train_in, train_out)
        neuron_groups = VGroup(*[
            layer.neurons
            for layer in network_mob.layers[1:]
        ])
        delta_neuron_groups = neuron_groups.copy()
        edge_groups = network_mob.edge_groups
        delta_edge_groups = VGroup(*[
            edge_group.copy()
            for edge_group in edge_groups
        ])
        tups = list(zip(
            it.count(), nabla_b, nabla_w, 
            delta_neuron_groups, neuron_groups,
            delta_edge_groups, edge_groups
        ))
        pc_color = self.positive_change_color
        nc_color = self.negative_change_color
        for i, nb, nw, delta_neurons, neurons, delta_edges, edges in reversed(tups):
            shown_nw = self.get_adjusted_first_matrix(nw)
            if np.max(shown_nw) == 0:
                shown_nw = (2*np.random.random(shown_nw.shape)-1)**5
            max_b = np.max(np.abs(nb))
            max_w = np.max(np.abs(shown_nw))
            for neuron, b in zip(delta_neurons, nb):
                color = nc_color if b > 0 else pc_color
                # neuron.set_fill(color, abs(b)/max_b)
                neuron.set_stroke(color, 3)
            for edge, w in zip(delta_edges.split(), shown_nw.T.flatten()):
                edge.set_stroke(
                    nc_color if w > 0 else pc_color,
                    3*abs(w)/max_w
                )
                edge.rotate_in_place(np.pi)
            if i == 2:
                delta_edges.submobjects = [
                    delta_edges[j]
                    for j in np.argsort(shown_nw.T.flatten())
                ]
            network = self.network
            network.weights[i] -= self.eta*nw
            network.biases[i] -= self.eta*nb

            self.play(
                ShowCreation(
                    delta_edges, lag_ratio = 0
                ),
                FadeIn(delta_neurons),
                run_time = 0.5
            )
        edge_groups.save_state()
        self.color_network_edges()
        self.remove(edge_groups)
        self.play(*it.chain(
            [ReplacementTransform(
                edge_groups.saved_state, edge_groups,
            )],
            list(map(FadeOut, [delta_edge_groups, delta_neuron_groups])),
            added_outro_anims,
        ))

    #####

    def get_adjusted_first_matrix(self, matrix):
        n = self.network_mob.max_shown_neurons
        if matrix.shape[1] > n:
            half = matrix.shape[1]/2
            return matrix[:,half-n/2:half+n/2]
        else:
            return matrix

    def color_network_edges(self):
        layers = self.network_mob.layers
        weight_matrices = self.network.weights
        for layer, matrix in zip(layers[1:], weight_matrices):
            matrix = self.get_adjusted_first_matrix(matrix)
            matrix_max = np.max(matrix)
            for neuron, row in zip(layer.neurons, matrix):
                for edge, w in zip(neuron.edges_in, row):
                    if w > 0:
                        color = self.positive_edge_color
                    else:
                        color = self.negative_edge_color
                    msw = self.max_stroke_width
                    swe = self.stroke_width_exp
                    sw = msw*(abs(w)/matrix_max)**swe
                    sw = min(sw, msw)
                    edge.set_stroke(color, sw)

    def get_edge_animation(self):
        edges = VGroup(*it.chain(*self.network_mob.edge_groups))
        return LaggedStartMap(
            ApplyFunction, edges,
            lambda mob : (
                lambda m : m.rotate_in_place(np.pi/12).set_color(YELLOW),
                mob
            ),
            rate_func = wiggle
        )

class BackpropComingLaterWords(Scene):
    def construct(self):
        words = TextMobject("(Backpropagation be \\\\ the next video)")
        words.set_width(FRAME_WIDTH-1)
        words.to_edge(DOWN)
        self.add(words)

class TrainingVsTestData(Scene):
    CONFIG = {
        "n_examples" : 10,
        "n_new_examples_shown" : 10,
    }
    def construct(self):
        self.initialize_data()
        self.introduce_all_data()
        self.subdivide_into_training_and_testing()
        self.scroll_through_much_data()

    def initialize_data(self):
        training_data, validation_data, test_data = load_data_wrapper()
        self.data = training_data
        self.curr_index = 0

    def get_examples(self):
        ci = self.curr_index
        self.curr_index += self.n_examples
        group = Group(*it.starmap(
            get_training_image_group,
            self.data[ci:ci+self.n_examples]
        ))
        group.arrange(DOWN)
        group.scale(0.5)
        return group

    def introduce_all_data(self):
        training_examples, test_examples = [
            self.get_examples() for x in range(2)
        ]

        training_examples.next_to(ORIGIN, LEFT)
        test_examples.next_to(ORIGIN, RIGHT)
        self.play(
            LaggedStartMap(FadeIn, training_examples),
            LaggedStartMap(FadeIn, test_examples),
        )

        self.training_examples = training_examples
        self.test_examples = test_examples

    def subdivide_into_training_and_testing(self):
        training_examples = self.training_examples
        test_examples = self.test_examples
        for examples in training_examples, test_examples:
            examples.generate_target()
        training_examples.target.shift(2*LEFT)
        test_examples.target.shift(2*RIGHT)

        train_brace = Brace(training_examples.target, LEFT)
        train_words = train_brace.get_text("Train on \\\\ these")
        test_brace = Brace(test_examples.target, RIGHT)
        test_words = test_brace.get_text("Test on \\\\ these")

        bools = [True]*(len(test_examples)-1) + [False]
        random.shuffle(bools)
        marks = VGroup()
        for is_correct, test_example in zip(bools, test_examples.target):
            if is_correct:
                mark = TexMobject("\\checkmark")
                mark.set_color(GREEN)
            else:
                mark = TexMobject("\\times")
                mark.set_color(RED)
            mark.next_to(test_example, LEFT)
            marks.add(mark)

        self.play(
            MoveToTarget(training_examples),
            GrowFromCenter(train_brace),
            FadeIn(train_words)
        )
        self.wait()
        self.play(
            MoveToTarget(test_examples),
            GrowFromCenter(test_brace),
            FadeIn(test_words)
        )
        self.play(Write(marks))
        self.wait()

    def scroll_through_much_data(self):
        training_examples = self.training_examples
        colors = color_gradient([BLUE, YELLOW], self.n_new_examples_shown)
        for color in colors:
            new_examples = self.get_examples()
            new_examples.move_to(training_examples)
            for train_ex, new_ex in zip(training_examples, new_examples):
                self.remove(train_ex)
                self.add(new_ex)
                new_ex[0][0].set_color(color)
                self.wait(1./30)
            training_examples = new_examples

class MNistDescription(Scene):
    CONFIG = {
        "n_grids" : 5,
        "n_rows_per_grid" : 10,
        "n_cols_per_grid" : 8,
        "time_per_example" : 1./120,
    }
    def construct(self):
        title = TextMobject("MNIST Database")
        title.scale(1.5)
        title.set_color(BLUE)
        authors = TextMobject("LeCun, Cortes and Burges")
        authors.next_to(title, DOWN)
        link_words = TextMobject("(links in the description)")
        link_words.next_to(authors, DOWN, MED_LARGE_BUFF)
        arrows = VGroup(*[Vector(DOWN) for x in range(4)])
        arrows.arrange(RIGHT, buff = LARGE_BUFF)
        arrows.next_to(link_words, DOWN)
        arrows.set_color(BLUE)

        word_group = VGroup(title, authors, link_words, arrows)
        word_group.center()

        self.add(title, authors)
        self.play(
            Write(link_words, run_time = 2),
            LaggedStartMap(GrowArrow, arrows),
        )
        self.wait()

        training_data, validation_data, test_data = load_data_wrapper()
        epc = self.n_rows_per_grid*self.n_cols_per_grid
        training_data_groups = [
            training_data[i*epc:(i+1)*epc]
            for i in range(self.n_grids)
        ]

        for i, td_group in enumerate(training_data_groups):
            print(i)
            group = Group(*[
                self.get_digit_pair(v_in, v_out)
                for v_in, v_out in td_group
            ])
            group.arrange_in_grid(
                n_rows = self.n_rows_per_grid,
            )
            group.set_height(FRAME_HEIGHT - 1)
            if i == 0:
                self.play(
                    LaggedStartMap(FadeIn, group),
                    FadeOut(word_group),
                )
            else:
                pairs = list(zip(last_group, group))
                random.shuffle(pairs)
                time = 0
                for t1, t2 in pairs:
                    time += self.time_per_example
                    self.remove(t1)
                    self.add(t2)
                    if time > self.frame_duration:
                        self.wait(self.frame_duration)
                        time = 0
            last_group = group


    def get_digit_pair(self, v_in, v_out):
        tup = Group(*TexMobject("(", "00", ",", "0", ")"))
        tup.scale(2)
        # image = PixelsFromVect(v_in)
        # image.add(SurroundingRectangle(image, color = BLUE, buff = SMALL_BUFF))
        image = MNistMobject(v_in)
        label = TexMobject(str(np.argmax(v_out)))
        image.replace(tup[1])
        tup.submobjects[1] = image
        label.replace(tup[3], dim_to_match = 1)
        tup.submobjects[3] = label

        return tup

class NotSciFi(TeacherStudentsScene):
    def construct(self):
        students = self.students
        self.student_says(
            "Machines learning?!?",
            student_index = 0,
            target_mode = "pleading",
            run_time = 1,
        )
        bubble = students[0].bubble
        students[0].bubble = None
        self.student_says(
            "Should we \\\\ be worried?", student_index = 2,
            target_mode = "confused",
            bubble_kwargs = {"direction" : LEFT},
            run_time = 1,
        )
        self.wait()
        students[0].bubble = bubble
        self.teacher_says(
            "It's actually \\\\ just calculus.",
            run_time = 1
        )
        self.teacher.bubble = None
        self.wait()
        self.student_says(
            "Even worse!", 
            target_mode = "horrified",
            bubble_kwargs = {
                "direction" : LEFT, 
                "width" : 3,
                "height" : 2,
            },
        )
        self.wait(2)

class FunctionMinmization(GraphScene):
    CONFIG = {
        "x_labeled_nums" : list(range(-1, 10)),
    }
    def construct(self):
        self.setup_axes()
        title = TextMobject("Finding minima")
        title.to_edge(UP)
        self.add(title)

        def func(x):
            x -= 4.5
            return 0.03*(x**4 - 16*x**2) + 0.3*x + 4
        graph = self.get_graph(func)
        graph_label = self.get_graph_label(graph, "C(x)")
        self.add(graph, graph_label)

        dots = VGroup(*[
            Dot().move_to(self.input_to_graph_point(x, graph))
            for x in range(10)
        ])
        dots.set_color_by_gradient(YELLOW, RED)

        def update_dot(dot, dt):
            x = self.x_axis.point_to_number(dot.get_center())
            slope = self.slope_of_tangent(x, graph)
            x -= slope*dt
            dot.move_to(self.input_to_graph_point(x, graph))

        self.add(*[
            Mobject.add_updater(dot, update_dot)
            for dot in dots
        ])
        self.wait(10)

class ChangingDecimalWithColor(ChangingDecimal):
    def interpolate_mobject(self, alpha):
        ChangingDecimal.interpolate_mobject(self, alpha)
        num = self.number_update_func(alpha)
        self.decimal_number.set_fill(
            interpolate_color(BLACK, WHITE, 0.5+num*0.5), 
            opacity = 1
        )

class IntroduceCostFunction(PreviewLearning):
    CONFIG = {
        "max_stroke_width" : 2,
        "full_edges_exp" : 5,
        "n_training_examples" : 100,
        "bias_color" : MAROON_B
    }
    def construct(self):
        self.network_mob.shift(LEFT)
        self.isolate_one_neuron()
        self.reminder_of_weights_and_bias()
        self.bring_back_rest_of_network()
        self.feed_in_example()
        self.make_fun_of_output()
        self.need_a_cost_function()
        self.fade_all_but_last_layer()
        self.break_down_cost_function()
        self.show_small_cost_output()
        self.average_over_all_training_data()

    def isolate_one_neuron(self):
        network_mob = self.network_mob
        neurons = VGroup(*it.chain(*[
            layer.neurons
            for layer in network_mob.layers[1:]
        ]))
        edges = VGroup(*it.chain(*network_mob.edge_groups))
        neuron = network_mob.layers[1].neurons[7]
        neurons.remove(neuron)
        edges.remove(*neuron.edges_in)
        output_labels = network_mob.output_labels
        kwargs = {
            "lag_ratio" : 0.5,
            "run_time" : 2,
        }
        self.play(
            FadeOut(edges, **kwargs),
            FadeOut(neurons, **kwargs),
            FadeOut(output_labels, **kwargs),
            Animation(neuron),
            neuron.edges_in.set_stroke, None, 2,
        )

        self.neuron = neuron

    def reminder_of_weights_and_bias(self):
        neuron = self.neuron
        layer0 = self.network_mob.layers[0]
        active_layer0 = self.network_mob.get_active_layer(
            0, np.random.random(len(layer0.neurons))
        )
        prev_neurons = layer0.neurons

        weighted_edges = VGroup(*[
            self.color_edge_randomly(edge.copy(), exp = 1)
            for edge in neuron.edges_in
        ])

        formula = TexMobject(
            "=", "\\sigma(",
            "w_1", "a_1", "+",
            "w_2", "a_2", "+",
            "\\cdots", "+",
            "w_n", "a_n", "+", "b", ")"
        )
        w_labels = formula.get_parts_by_tex("w_")
        a_labels = formula.get_parts_by_tex("a_")
        b = formula.get_part_by_tex("b")
        sigma = VGroup(
            formula.get_part_by_tex("\\sigma"),
            formula.get_part_by_tex(")"),
        )
        symbols = VGroup(*[
            formula.get_parts_by_tex(tex)
            for tex in ("=", "+", "dots")
        ])

        w_labels.set_color(self.positive_edge_color)
        b.set_color(self.bias_color)
        sigma.set_color(YELLOW)
        formula.next_to(neuron, RIGHT)

        weights_word = TextMobject("Weights")
        weights_word.next_to(neuron.edges_in, RIGHT, aligned_edge = UP)
        weights_word.set_color(self.positive_edge_color)
        weights_arrow_to_edges = Arrow(
            weights_word.get_bottom(),
            neuron.edges_in[0].get_center(),
            color = self.positive_edge_color
        )

        weights_arrow_to_syms = VGroup(*[
            Arrow(
                weights_word.get_bottom(),
                w_label.get_top(),
                color = self.positive_edge_color
            )
            for w_label in w_labels
        ])

        bias_word = TextMobject("Bias")
        bias_arrow = Vector(DOWN, color = self.bias_color)
        bias_arrow.next_to(b, UP, SMALL_BUFF)
        bias_word.next_to(bias_arrow, UP, SMALL_BUFF)
        bias_word.set_color(self.bias_color)

        self.play(
            Transform(layer0, active_layer0),
            neuron.set_fill, None, 0.5,
            FadeIn(formula),
            run_time = 2,
            lag_ratio = 0.5
        )
        self.play(LaggedStartMap(
            ShowCreationThenDestruction, 
            neuron.edges_in.copy().set_stroke(YELLOW, 3),
            run_time = 1.5,
            lag_ratio = 0.7,
            remover = True
        ))
        self.play(
            Write(weights_word),
            *list(map(GrowArrow, weights_arrow_to_syms)),
            run_time = 1
        )
        self.wait()
        self.play(
            ReplacementTransform(
                w_labels.copy(), weighted_edges,
                remover = True
            ),
            Transform(neuron.edges_in, weighted_edges),
            ReplacementTransform(
                weights_arrow_to_syms,
                VGroup(weights_arrow_to_edges),
            )
        )
        self.wait()
        self.play(
            Write(bias_word),
            GrowArrow(bias_arrow),
            run_time = 1
        )
        self.wait(2)

        ## Initialize randomly
        w_random = TextMobject("Initialize randomly")
        w_random.move_to(weights_word, LEFT)
        b_random = w_random.copy()
        b_random.move_to(bias_word, RIGHT)

        self.play(
            Transform(weights_word, w_random),
            Transform(bias_word, b_random),
            *[
                ApplyFunction(self.color_edge_randomly, edge)
                for edge in neuron.edges_in
            ]
        )
        self.play(LaggedStartMap(
            ApplyMethod, neuron.edges_in,
            lambda m : (m.rotate_in_place, np.pi/12),
            rate_func = wiggle,
            run_time = 2
        ))
        self.play(*list(map(FadeOut, [
            weights_word, weights_arrow_to_edges,
            bias_word, bias_arrow,
            formula
        ])))

    def bring_back_rest_of_network(self):
        network_mob = self.network_mob
        neurons = VGroup(*network_mob.layers[1].neurons)
        neurons.remove(self.neuron)
        for layer in network_mob.layers[2:]:
            neurons.add(*layer.neurons)
        neurons.add(*network_mob.output_labels)

        edges = VGroup(*network_mob.edge_groups[0])
        edges.remove(*self.neuron.edges_in)
        for edge_group in network_mob.edge_groups[1:]:
            edges.add(*edge_group)

        for edge in edges:
            self.color_edge_randomly(edge, exp = self.full_edges_exp)

        self.play(*[
            LaggedStartMap(
                FadeIn, group,
                run_time = 3,
            )
            for group in (neurons, edges)
        ])

    def feed_in_example(self):
        vect = get_organized_images()[3][5]
        image = PixelsFromVect(vect)
        image.to_corner(UP+LEFT)
        rect = SurroundingRectangle(image, color = BLUE)
        neurons = VGroup(*[
            Circle(
                stroke_width = 1,
                stroke_color = WHITE,
                fill_opacity = pixel.fill_rgb[0],
                fill_color = WHITE,
                radius = pixel.get_height()/2
            ).move_to(pixel)
            for pixel in image
        ])
        layer0= self.network_mob.layers[0]
        n = self.network_mob.max_shown_neurons
        neurons.target = VGroup(*it.chain(
            VGroup(*layer0.neurons[:n/2]).set_fill(opacity = 0),
            [
                VectorizedPoint(layer0.dots.get_center())
                for x in range(len(neurons)-n)
            ],
            VGroup(*layer0.neurons[-n/2:]).set_fill(opacity = 0),
        ))

        self.play(
            self.network_mob.shift, 0.5*RIGHT,
            ShowCreation(rect),
            LaggedStartMap(DrawBorderThenFill, image),
            LaggedStartMap(DrawBorderThenFill, neurons),
            run_time = 1
        )
        self.play(
            MoveToTarget(
                neurons, lag_ratio = 0.5,
                remover = True
            ),
            layer0.neurons.set_fill, None, 0,
        )
        self.activate_network(vect, run_time = 2)

        self.image = image
        self.image_rect = rect

    def make_fun_of_output(self):
        last_layer = self.network_mob.layers[-1].neurons
        last_layer.add(self.network_mob.output_labels)
        rect = SurroundingRectangle(last_layer)
        words = TextMobject("Utter trash")
        words.next_to(rect, DOWN, aligned_edge = LEFT)
        VGroup(rect, words).set_color(YELLOW)

        self.play(
            ShowCreation(rect),
            Write(words, run_time = 2)
        )
        self.wait()

        self.trash_rect = rect
        self.trash_words = words

    def need_a_cost_function(self):
        vect = np.zeros(10)
        vect[3] = 1
        output_labels = self.network_mob.output_labels
        desired_layer = self.network_mob.get_active_layer(-1, vect)
        layer = self.network_mob.layers[-1]
        layer.add(output_labels)
        desired_layer.add(output_labels.copy())
        desired_layer.shift(2*RIGHT)
        layers = VGroup(layer, desired_layer)

        words = TextMobject(
            "What's the", "``cost''\\\\", "of this difference?",
        )
        words.set_color_by_tex("cost", RED)
        words.next_to(layers, UP)
        words.to_edge(UP)
        words.shift_onto_screen()
        double_arrow = DoubleArrow(
            layer.get_right(),
            desired_layer.get_left(),
            color = RED
        )

        self.play(FadeIn(words))
        self.play(ReplacementTransform(layer.copy(), desired_layer))
        self.play(GrowFromCenter(double_arrow))
        self.wait(2)

        self.desired_last_layer = desired_layer
        self.diff_arrow = double_arrow

    def fade_all_but_last_layer(self):
        network_mob = self.network_mob
        to_fade = VGroup(*it.chain(*list(zip(
            network_mob.layers[:-1],
            network_mob.edge_groups
        ))))

        self.play(LaggedStartMap(FadeOut, to_fade, run_time = 1))

    def break_down_cost_function(self):
        layer = self.network_mob.layers[-1]
        desired_layer = self.desired_last_layer
        decimal_groups = VGroup(*[
            self.num_vect_to_decimals(self.layer_to_num_vect(l))
            for l in (layer, desired_layer)
        ])

        terms = VGroup()
        symbols = VGroup()
        for d1, d2 in zip(*decimal_groups):
            term = TexMobject(
                "(", "0.00", "-", "0.00", ")^2", "+",
            )
            term.scale(d1.get_height()/term[1].get_height())
            for d, i in (d1, 1), (d2, 3):
                term.submobjects[i] = d.move_to(term[i])
            terms.add(term)
            symbols.add(*term)
            symbols.remove(d1, d2)
            last_plus = term[-1]
        for mob in terms[-1], symbols:
            mob.remove(last_plus)
        terms.arrange(
            DOWN, buff = SMALL_BUFF,
            aligned_edge = LEFT
        )
        terms.set_height(1.5*layer.get_height())
        terms.next_to(layer, LEFT, buff = 2)

        image_group = Group(self.image, self.image_rect)
        image_group.generate_target()
        image_group.target.scale(0.5)
        cost_of = TextMobject("Cost of").set_color(RED)
        cost_group = VGroup(cost_of, image_group.target)
        cost_group.arrange(RIGHT)
        brace = Brace(terms, LEFT)
        cost_group.next_to(brace, LEFT)

        self.revert_to_original_skipping_status()
        self.play(*[
            ReplacementTransform(
                VGroup(*l.neurons[:10]).copy(), dg
            )
            for l, dg in zip([layer, desired_layer], decimal_groups)
        ])
        self.play(
            FadeIn(symbols),
            MoveToTarget(image_group),
            FadeIn(cost_of),
            GrowFromCenter(brace),
        )
        self.wait()

        self.decimal_groups = decimal_groups
        self.image_group = image_group
        self.cost_group = VGroup(cost_of, image_group)
        self.brace = brace

    def show_small_cost_output(self):
        decimals, desired_decimals = self.decimal_groups
        neurons = self.network_mob.layers[-1].neurons
        brace = self.brace
        cost_group = self.cost_group

        neurons.save_state()
        cost_group.save_state()
        brace.save_state()
        brace.generate_target()

        arrows = VGroup(*[
            Arrow(ORIGIN, LEFT).next_to(d, LEFT, MED_LARGE_BUFF)
            for d in decimals
        ])
        arrows.set_color(WHITE)

        def generate_term_update_func(decimal, desired_decimal):
            return lambda a : (decimal.number - desired_decimal.number)**2

        terms = VGroup()
        term_updates = []
        for arrow, d1, d2 in zip(arrows, *self.decimal_groups):
            term = DecimalNumber(0, num_decimal_places = 4)
            term.set_height(d1.get_height())
            term.next_to(arrow, LEFT)
            term.num_update_func = generate_term_update_func(d1, d2)
            terms.add(term)
            term_updates.append(ChangingDecimalWithColor(
                term, term.num_update_func,
                num_decimal_places = 4
            ))
        brace.target.next_to(terms, LEFT)

        sum_term = DecimalNumber(0)
        sum_term.next_to(brace.target, LEFT)
        sum_term.set_color(RED)
        def sum_update(alpha):
            return sum([
                (d1.number - d2.number)**2
                for d1, d2 in zip(*self.decimal_groups)
            ])
        term_updates.append(ChangingDecimal(sum_term, sum_update))
        for update in term_updates:
            update.interpolate_mobject(0)

        target_vect = 0.1*np.random.random(10)
        target_vect[3] = 0.97

        def generate_decimal_update_func(start, target):
            return lambda a : interpolate(start, target, a)

        update_decimals = [
            ChangingDecimalWithColor(
                decimal, 
                generate_decimal_update_func(decimal.number, t)
            )
            for decimal, t in zip(decimals, target_vect)
        ]

        self.play(
            cost_group.scale, 0.5,
            cost_group.to_corner, UP+LEFT,
            MoveToTarget(brace),
            LaggedStartMap(GrowArrow, arrows),
            LaggedStartMap(FadeIn, terms),
            FadeIn(sum_term),
            Animation(decimals)
        )
        self.play(*it.chain(
            update_decimals,
            term_updates,
            [
                ApplyMethod(neuron.set_fill, None, t)
                for neuron, t in zip(neurons, target_vect)
            ]
        ))
        self.wait()
        self.play(LaggedStartMap(Indicate, decimals, rate_func = there_and_back))
        self.wait()
        for update in update_decimals:
            update.rate_func = lambda a : smooth(1-a)
        self.play(*it.chain(
            update_decimals,
            term_updates,
            [neurons.restore]
        ), run_time = 2)
        self.wait()
        self.play(
            cost_group.restore,
            brace.restore,
            FadeOut(VGroup(terms, sum_term, arrows)),
        )

    def average_over_all_training_data(self):
        image_group = self.image_group
        decimal_groups = self.decimal_groups

        random_neurons = self.network_mob.layers[-1].neurons
        desired_neurons = self.desired_last_layer.neurons

        wait_times = iter(it.chain(
            4*[0.5],
            4*[0.25],
            8*[0.125],
            it.repeat(0.1)
        ))

        words = TextMobject("Average cost of \\\\ all training data...")
        words.set_color(BLUE)
        words.to_corner(UP+LEFT)

        self.play(
            Write(words, run_time = 1),
        )

        training_data, validation_data, test_data = load_data_wrapper()
        for in_vect, out_vect in training_data[:self.n_training_examples]:
            random_v = np.random.random(10)
            new_decimal_groups = VGroup(*[
                self.num_vect_to_decimals(v)
                for v in (random_v, out_vect)
            ])
            for ds, nds in zip(decimal_groups, new_decimal_groups):
                for old_d, new_d in zip(ds, nds):
                    new_d.replace(old_d)
            self.remove(decimal_groups)
            self.add(new_decimal_groups)
            decimal_groups = new_decimal_groups
            for pair in (random_v, random_neurons), (out_vect, desired_neurons):
                for n, neuron in zip(*pair):
                    neuron.set_fill(opacity = n)
            new_image_group = MNistMobject(in_vect)
            new_image_group.replace(image_group)
            self.remove(image_group)
            self.add(new_image_group)
            image_group = new_image_group

            self.wait(next(wait_times))

    ####

    def color_edge_randomly(self, edge, exp = 1):
        r = (2*np.random.random()-1)**exp
        r *= self.max_stroke_width
        pc, nc = self.positive_edge_color, self.negative_edge_color
        edge.set_stroke(
            color = pc if r > 0 else nc,
            width = abs(r),
        )
        return edge

    def layer_to_num_vect(self, layer, n_terms = 10):
        return [
            n.get_fill_opacity()
            for n in layer.neurons
        ][:n_terms]

    def num_vect_to_decimals(self, num_vect):
        return VGroup(*[
            DecimalNumber(n).set_fill(opacity = 0.5*n + 0.5)
            for n in num_vect
        ])

    def num_vect_to_column_vector(self, num_vect, height):
        decimals = VGroup(*[
            DecimalNumber(n).set_fill(opacity = 0.5*n + 0.5)
            for n in num_vect
        ])
        decimals.arrange(DOWN)
        decimals.set_height(height)
        lb, rb = brackets = TexMobject("[]")
        brackets.scale(2)
        brackets.stretch_to_fit_height(height + SMALL_BUFF)
        lb.next_to(decimals, LEFT)
        rb.next_to(decimals, RIGHT)
        result = VGroup(brackets, decimals)
        result.brackets = brackets
        result.decimals = decimals
        return result

class YellAtNetwork(PiCreatureScene, PreviewLearning):
    def setup(self):
        PiCreatureScene.setup(self)
        PreviewLearning.setup(self)

    def construct(self):
        randy = self.randy
        network_mob, eyes = self.get_network_and_eyes()

        three_vect = get_organized_images()[3][5]
        self.activate_network(three_vect)

        image = PixelsFromVect(three_vect)
        image.add(SurroundingRectangle(image, color = BLUE))
        arrow = Arrow(LEFT, RIGHT, color = WHITE)

        layer = network_mob.layers[-1]
        layer.add(network_mob.output_labels)
        layer_copy = layer.deepcopy()
        for neuron in layer_copy.neurons:
            neuron.set_fill(WHITE, opacity = 0)
        layer_copy.neurons[3].set_fill(WHITE, 1)
        layer_copy.scale(1.5)
        desired = Group(image, arrow, layer_copy)
        desired.arrange(RIGHT)
        desired.to_edge(UP)

        q_marks = TexMobject("???").set_color(RED)
        q_marks.next_to(arrow, UP, SMALL_BUFF)

        self.play(
            PiCreatureBubbleIntroduction(
                randy, "Bad network!",
                target_mode = "angry", 
                look_at_arg = eyes,
                run_time = 1,
            ),
            eyes.look_at_anim(randy.eyes)
        )
        self.play(eyes.change_mode_anim("sad"))
        self.play(eyes.look_at_anim(3*DOWN + 3*RIGHT))
        self.play(
            FadeIn(desired),
            RemovePiCreatureBubble(
                randy, target_mode = "sassy",
                look_at_arg = desired
            ),
            eyes.look_at_anim(desired)
        )
        self.play(eyes.blink_anim())
        rect = SurroundingRectangle(
            VGroup(layer_copy.neurons[3], layer_copy[-1][3]),
        )
        self.play(ShowCreation(rect))
        layer_copy.add(rect)
        self.wait()
        self.play(
            layer.copy().replace, layer_copy, 1,
            Write(q_marks, run_time = 1),
            layer_copy.shift, 1.5*RIGHT,
            randy.change, "angry", eyes,
        )
        self.play(eyes.look_at_anim(3*RIGHT + 3*RIGHT))
        self.wait()

    ####

    def get_network_and_eyes(self):
        randy = self.randy
        network_mob = self.network_mob
        network_mob.scale(0.5)
        network_mob.next_to(randy, RIGHT, LARGE_BUFF)
        self.color_network_edges()
        eyes = Eyes(network_mob.edge_groups[1])
        return network_mob, eyes

    def create_pi_creature(self):
        randy = self.randy = Randolph()
        randy.shift(3*LEFT).to_edge(DOWN)
        return randy

class ThisIsVeryComplicated(TeacherStudentsScene):
    def construct(self):
        self.teacher_says(
            "Very complicated!",
            target_mode = "surprised",
            run_time = 1,
        )
        self.change_student_modes(*3*["guilty"])
        self.wait(2)

class EmphasizeComplexityOfCostFunction(IntroduceCostFunction):
    CONFIG = {
        "stroke_width_exp" : 3,
        "n_examples" : 32,
    }
    def construct(self):
        self.setup_sides()
        self.show_network_as_a_function()
        self.show_cost_function()

    def setup_sides(self):
        v_line = Line(UP, DOWN).scale(FRAME_Y_RADIUS)
        network_mob = self.network_mob
        network_mob.set_width(FRAME_X_RADIUS - 1)
        network_mob.to_corner(DOWN+LEFT)

        self.add(v_line)
        self.color_network_edges()

    def show_network_as_a_function(self):
        title = TextMobject("Neural network function")
        title.shift(FRAME_X_RADIUS*RIGHT/2)
        title.to_edge(UP)
        underline = Line(LEFT, RIGHT)
        underline.stretch_to_fit_width(title.get_width())
        underline.next_to(title, DOWN, SMALL_BUFF)
        self.add(title, underline)

        words = self.get_function_description_words(
            "784 numbers (pixels)",
            "10 numbers",
            "13{,}002 weights/biases",
        )
        input_words, output_words, parameter_words = words
        for word in words:
            self.add(word[0])

        in_vect = get_organized_images()[7][8]
        activations = self.network.get_activation_of_all_layers(in_vect)
        image = MNistMobject(in_vect)
        image.set_height(1.5)
        image_label = TextMobject("Input")
        image_label.set_color(input_words[0].get_color())
        image_label.next_to(image, UP, SMALL_BUFF)

        arrow = Arrow(LEFT, RIGHT, color = WHITE)
        arrow.next_to(image, RIGHT)
        output = self.num_vect_to_column_vector(activations[-1], 2)
        output.next_to(arrow, RIGHT)

        group = Group(image, image_label, arrow, output)
        group.next_to(self.network_mob, UP, 0, RIGHT)

        dot = Dot()
        dot.move_to(input_words.get_right())
        dot.set_fill(opacity = 0.5)

        self.play(FadeIn(input_words[1], lag_ratio = 0.5))
        self.play(
            dot.move_to, image,
            dot.set_fill, None, 0,
            FadeIn(image),
            FadeIn(image_label),
        )
        self.activate_network(in_vect, 
            GrowArrow(arrow),
            FadeIn(output),
            FadeIn(output_words[1])
        )
        self.wait()
        self.play(
            FadeIn(parameter_words[1]), 
            self.get_edge_animation()
        )
        self.wait(2)

        self.to_fade = group
        self.curr_words = words
        self.title = title
        self.underline = underline

    def show_cost_function(self):
        network_mob = self.network_mob
        to_fade = self.to_fade
        input_words, output_words, parameter_words = self.curr_words

        network_mob.generate_target()
        network_mob.target.scale_in_place(0.7)
        network_mob.target.to_edge(UP, buff = LARGE_BUFF)
        rect = SurroundingRectangle(network_mob.target, color = BLUE)
        network_label = TextMobject("Input")
        network_label.set_color(input_words[0].get_color())
        network_label.next_to(rect, UP, SMALL_BUFF)

        new_output_word = TextMobject("1 number", "(the cost)")
        new_output_word[1].set_color(RED).scale(0.9)
        new_output_word.move_to(output_words[1], LEFT)
        new_output_word.shift(0.5*SMALL_BUFF*DOWN)
        new_parameter_word = TextMobject("""
            \\begin{flushleft}
            Many, many, many \\\\ training examples
            \\end{flushleft}
        """).scale(0.9)
        new_parameter_word.move_to(parameter_words[1], UP+LEFT) 

        new_title = TextMobject("Cost function")
        new_title.set_color(RED)
        new_title.move_to(self.title)

        arrow = Arrow(UP, DOWN, color = WHITE)
        arrow.next_to(rect, DOWN)
        cost = TextMobject("Cost: 5.4")
        cost.set_color(RED)
        cost.next_to(arrow, DOWN)

        training_data, validation_data, test_data = load_data_wrapper()
        training_examples = Group(*list(map(
            self.get_training_pair_mob, 
            training_data[:self.n_examples]
        )))
        training_examples.next_to(parameter_words, DOWN, buff = LARGE_BUFF)

        self.play(
            FadeOut(to_fade),
            FadeOut(input_words[1]),
            FadeOut(output_words[1]),
            MoveToTarget(network_mob),
            FadeIn(rect),
            FadeIn(network_label),
            Transform(self.title, new_title),
            self.underline.stretch_to_fit_width, new_title.get_width()
        )
        self.play(
            ApplyMethod(
                parameter_words[1].move_to, input_words[1], LEFT,
                path_arc = np.pi,
            ),
            self.get_edge_animation()
        )
        self.wait()
        self.play(
            GrowArrow(arrow),
            Write(cost, run_time = 1)
        )
        self.play(Write(new_output_word, run_time = 1))
        self.wait()
        self.play(
            FadeIn(new_parameter_word),
            FadeIn(training_examples[0])
        )
        self.wait(0.5)
        for last_ex, ex in zip(training_examples, training_examples[1:]):
            activations = self.network.get_activation_of_all_layers(
                ex.in_vect
            )
            for i, a in enumerate(activations):
                layer = self.network_mob.layers[i]
                active_layer = self.network_mob.get_active_layer(i, a)
                Transform(layer, active_layer).update(1)
            self.remove(last_ex)
            self.add(ex)
            self.wait(0.25)

    ####

    def get_function_description_words(self, w1, w2, w3):
        input_words = TextMobject("Input:", w1)
        input_words[0].set_color(BLUE)
        output_words = TextMobject("Output:", w2)
        output_words[0].set_color(YELLOW)
        parameter_words = TextMobject("Parameters:", w3)
        parameter_words[0].set_color(GREEN)
        words = VGroup(input_words, output_words, parameter_words)
        words.arrange(DOWN, aligned_edge = LEFT)
        words.scale(0.9)
        words.next_to(ORIGIN, RIGHT)
        words.shift(UP)
        return words

    def get_training_pair_mob(self, data):
        in_vect, out_vect = data
        image = MNistMobject(in_vect)
        image.set_height(1)
        comma = TextMobject(",")
        comma.next_to(image, RIGHT, SMALL_BUFF, DOWN)
        output = TexMobject(str(np.argmax(out_vect)))
        output.set_height(0.75)
        output.next_to(image, RIGHT, MED_SMALL_BUFF)
        lp, rp = parens = TextMobject("()")
        parens.scale(2)
        parens.stretch_to_fit_height(1.2*image.get_height())
        lp.next_to(image, LEFT, SMALL_BUFF)
        rp.next_to(lp, RIGHT, buff = 2)

        result = Group(lp, image, comma, output, rp)
        result.in_vect = in_vect
        return result

class NetworkGrowthMindset(YellAtNetwork):
    def construct(self):
        randy = self.pi_creature
        network_mob, eyes = self.get_network_and_eyes()
        eyes.look_at_anim(randy.eyes).update(1)
        edge_update = ContinualEdgeUpdate(
            network_mob,
            colors = [BLUE, RED]
        )

        self.play(
            PiCreatureSays(
                randy, "Awful, just awful!",
                target_mode = "angry",
                look_at_arg = eyes,
                run_time = 1,
            ),
            eyes.change_mode_anim("concerned_musician")
        )
        self.wait()
        self.add(edge_update)
        self.pi_creature_says(
            "But we can do better! \\\\ Growth mindset!",
            target_mode = "hooray"
        )
        self.play(eyes.change_mode_anim("happy"))
        self.wait(3)

class SingleVariableCostFunction(GraphScene):
    CONFIG = {
        "x_axis_label" : "$w$",
        "y_axis_label" : "",
        "x_min" : -5,
        "x_max" : 7,
        "x_axis_width" : 12,
        "graph_origin" : 2.5*DOWN + LEFT,
        "tangent_line_color" : YELLOW,
    }
    def construct(self):
        self.reduce_full_function_to_single_variable()
        self.show_graph()
        self.find_exact_solution()
        self.make_function_more_complicated()
        self.take_steps()
        self.take_steps_based_on_slope()
        self.ball_rolling_down_hill()
        self.note_step_sizes()

    def reduce_full_function_to_single_variable(self):
        name = TextMobject("Cost function")
        cf1 = TexMobject("C(", "w_1, w_2, \\dots, w_{13{,}002}", ")")
        cf2 = TexMobject("C(", "w", ")")
        for cf in cf1, cf2:
            VGroup(cf[0], cf[2]).set_color(RED)
        big_brace, lil_brace = [
            Brace(cf[1], DOWN)
            for cf in (cf1, cf2)
        ]
        big_brace_text = big_brace.get_text("Weights and biases")
        lil_brace_text = lil_brace.get_text("Single input")

        name.next_to(cf1, UP, LARGE_BUFF)
        name.set_color(RED)

        self.add(name, cf1)
        self.play(
            GrowFromCenter(big_brace),
            FadeIn(big_brace_text)
        )
        self.wait()
        self.play(
            ReplacementTransform(big_brace, lil_brace),
            ReplacementTransform(big_brace_text, lil_brace_text),
            ReplacementTransform(cf1, cf2),
        )

        # cf2.add_background_rectangle()
        lil_brace_text.add_background_rectangle()
        self.brace_group = VGroup(lil_brace, lil_brace_text)
        cf2.add(self.brace_group)
        self.function_label = cf2
        self.to_fade = name

    def show_graph(self):
        function_label = self.function_label
        self.setup_axes()
        graph = self.get_graph(
            lambda x : 0.5*(x - 3)**2 + 2,
            color = RED
        )

        self.play(
            FadeOut(self.to_fade),
            Write(self.axes),
            Animation(function_label),
            run_time = 1,
        )
        self.play(
            function_label.next_to, 
                self.input_to_graph_point(5, graph), RIGHT,
            ShowCreation(graph)
        )
        self.wait()

        self.graph = graph

    def find_exact_solution(self):
        function_label = self.function_label
        graph = self.graph

        w_min = TexMobject("w", "_{\\text{min}}", arg_separator = "")
        w_min.move_to(function_label[1], UP+LEFT)
        w_min[1].fade(1)
        x = 3
        dot = Dot(
            self.input_to_graph_point(x, graph),
            color = YELLOW
        )
        line = self.get_vertical_line_to_graph(
            x, graph, 
            line_class = DashedLine,
            color = YELLOW
        )
        formula = TexMobject("\\frac{dC}{dw}(w) = 0")
        formula.next_to(dot, UP, buff = 2)
        formula.shift(LEFT)
        arrow = Arrow(formula.get_bottom(), dot.get_center())

        self.play(
            w_min.shift, 
                line.get_bottom() - w_min[0].get_top(),
                MED_SMALL_BUFF*DOWN,
            w_min.set_fill, WHITE, 1,
        )
        self.play(ShowCreation(line))
        self.play(DrawBorderThenFill(dot, run_time = 1))
        self.wait()
        self.play(Write(formula, run_time = 2))
        self.play(GrowArrow(arrow))
        self.wait()

        self.dot = dot
        self.line = line
        self.w_min = w_min
        self.deriv_group = VGroup(formula, arrow)

    def make_function_more_complicated(self):
        dot = self.dot
        line = self.line
        w_min = self.w_min
        deriv_group = self.deriv_group
        function_label = self.function_label
        brace_group = function_label[-1]
        function_label.remove(brace_group)

        brace = Brace(deriv_group, UP)
        words = TextMobject("Sometimes \\\\ infeasible")
        words.next_to(deriv_group, UP)
        words.set_color(BLUE)
        words.next_to(brace, UP)

        graph = self.get_graph(
            lambda x : 0.05*((x+2)*(x-1)*(x-3))**2 + 2 + 0.3*(x-3),
            color = RED
        )

        self.play(
            ReplacementTransform(self.graph, graph),
            function_label.shift, 2*UP+1.9*LEFT,
            FadeOut(brace_group),
            Animation(dot)
        )
        self.graph = graph
        self.play(
            Write(words, run_time = 1),
            GrowFromCenter(brace)
        )
        self.wait(2)
        self.play(FadeOut(VGroup(words, brace, deriv_group)))

    def take_steps(self):
        dot = self.dot
        line = self.line
        w_mob, min_mob = self.w_min
        graph = self.graph

        def update_line(line):
            x = self.x_axis.point_to_number(w_mob.get_center())
            line.put_start_and_end_on_with_projection(
                self.coords_to_point(x, 0),
                self.input_to_graph_point(x, graph)
            )
            return line
        line_update_anim = UpdateFromFunc(line, update_line)

        def update_dot(dot):
            dot.move_to(line.get_end())
            return dot
        dot_update_anim = UpdateFromFunc(dot, update_dot)

        point = self.coords_to_point(2, 0)
        arrows = VGroup()
        q_marks = VGroup()
        for vect, color in (LEFT, BLUE), (RIGHT, GREEN):
            arrow = Arrow(ORIGIN, vect, buff = SMALL_BUFF)
            arrow.shift(point + SMALL_BUFF*UP)
            arrow.set_color(color)
            arrows.add(arrow)
            q_mark = TextMobject("?")
            q_mark.next_to(arrow, UP, buff = 0)
            q_mark.add_background_rectangle()
            q_marks.add(q_mark)

        self.play(
            w_mob.next_to, point, DOWN,
            FadeOut(min_mob),
            line_update_anim, 
            dot_update_anim,
        )
        self.wait()
        self.play(*it.chain(
            list(map(GrowArrow, arrows)),
            list(map(FadeIn, q_marks)),
        ))
        self.wait()

        self.arrow_group = VGroup(arrows, q_marks)
        self.line_update_anim = line_update_anim
        self.dot_update_anim = dot_update_anim
        self.w_mob = w_mob

    def take_steps_based_on_slope(self):
        arrows, q_marks = arrow_group = self.arrow_group
        line_update_anim = self.line_update_anim
        dot_update_anim = self.dot_update_anim
        dot = self.dot
        w_mob = self.w_mob
        graph = self.graph

        x = self.x_axis.point_to_number(w_mob.get_center())
        tangent_line = self.get_tangent_line(x, arrows[0].get_color())

        self.play(
            ShowCreation(tangent_line),
            Animation(dot),
        )
        self.play(VGroup(arrows[1], q_marks).set_fill, None, 0)
        self.play(
            w_mob.shift, MED_SMALL_BUFF*LEFT,
            MaintainPositionRelativeTo(arrow_group, w_mob),
            line_update_anim, dot_update_anim,
        )
        self.wait()

        new_x = 0.3
        new_point = self.coords_to_point(new_x, 0)
        new_tangent_line = self.get_tangent_line(
            new_x, arrows[1].get_color()
        )
        self.play(
            FadeOut(tangent_line),
            w_mob.next_to, new_point, DOWN,
            arrow_group.next_to, new_point, UP, SMALL_BUFF,
            arrow_group.set_fill, None, 1,
            dot_update_anim,
            line_update_anim,
        )
        self.play(
            ShowCreation(new_tangent_line),
            Animation(dot),
            Animation(arrow_group),
        )
        self.wait()
        self.play(VGroup(arrows[0], q_marks).set_fill, None, 0)
        self.play(
            w_mob.shift, MED_SMALL_BUFF*RIGHT,
            MaintainPositionRelativeTo(arrow_group, w_mob),
            line_update_anim, dot_update_anim,
        )
        self.play(
            FadeOut(VGroup(new_tangent_line, arrow_group)),
            Animation(dot),
        )
        self.wait()
        for x in 0.8, 1.1, 0.95:
            self.play(
                w_mob.next_to, self.coords_to_point(x, 0), DOWN,
                line_update_anim,
                dot_update_anim,
            )
        self.wait()

    def ball_rolling_down_hill(self):
        ball = self.dot
        graph = self.graph
        point = VectorizedPoint(self.coords_to_point(-0.5, 0))
        w_mob = self.w_mob

        def update_ball(ball):
            x = self.x_axis.point_to_number(ball.point.get_center())
            graph_point = self.input_to_graph_point(x, graph)
            vect = rotate_vector(UP, self.angle_of_tangent(x, graph))
            radius = ball.get_width()/2
            ball.move_to(graph_point + radius*vect)
            return ball

        def update_point(point, dt):
            x = self.x_axis.point_to_number(point.get_center())
            slope = self.slope_of_tangent(x, graph)
            if abs(slope) > 0.5:
                slope = 0.5 * slope / abs(slope)
            x -= slope*dt
            point.move_to(self.coords_to_point(x, 0))


        ball.generate_target()
        ball.target.scale(2)
        ball.target.set_fill(opacity = 0)
        ball.target.set_stroke(BLUE, 3)
        ball.point = point
        ball.target.point = point
        update_ball(ball.target)

        self.play(MoveToTarget(ball))
        self.play(
            point.move_to, w_mob,
            UpdateFromFunc(ball, update_ball),
            run_time = 3,
        )
        self.wait(2)

        points = [
            VectorizedPoint(self.coords_to_point(x, 0))
            for x in np.linspace(-2.7, 3.7, 11)
        ]
        balls = VGroup()
        updates = []
        for point in points:
            new_ball = ball.copy() 
            new_ball.point = point
            balls.add(new_ball)
            updates += [
                Mobject.add_updater(point, update_point),
                Mobject.add_updater(new_ball, update_ball)
            ]
        balls.set_color_by_gradient(BLUE, GREEN)

        self.play(ReplacementTransform(ball, balls))
        self.add(*updates)
        self.wait(5)
        self.remove(*updates)
        self.remove(*points)
        self.play(FadeOut(balls))

    def note_step_sizes(self):
        w_mob = self.w_mob
        line_update_anim = self.line_update_anim

        x = -0.5
        target_x = 0.94
        point = VectorizedPoint(self.coords_to_point(x, 0))
        line = self.get_tangent_line(x)
        line.scale_in_place(0.5)
        def update_line(line):
            x = self.x_axis.point_to_number(point.get_center())
            self.make_line_tangent(line, x)
            return line

        self.play(
            ShowCreation(line),
            w_mob.next_to, point, DOWN,
            line_update_anim,
        )
        for n in range(6):
            x = self.x_axis.point_to_number(point.get_center())
            new_x = interpolate(x, target_x, 0.5)
            self.play(
                point.move_to, self.coords_to_point(new_x, 0),
                MaintainPositionRelativeTo(w_mob, point),
                line_update_anim,
                UpdateFromFunc(line, update_line),
            )
            self.wait(0.5)
        self.wait()

    ###

    def get_tangent_line(self, x, color = YELLOW):
        tangent_line = Line(LEFT, RIGHT).scale(3)
        tangent_line.set_color(color)
        self.make_line_tangent(tangent_line, x)
        return tangent_line

    def make_line_tangent(self, line, x):
        graph = self.graph
        line.rotate(self.angle_of_tangent(x, graph) - line.get_angle())
        line.move_to(self.input_to_graph_point(x, graph))

class LocalVsGlobal(TeacherStudentsScene):
    def construct(self):
        self.teacher_says("""
            Local minimum = Doable \\\\
            Global minimum = Crazy hard
        """)
        self.change_student_modes(*["pondering"]*3)
        self.wait(2)

class TwoVariableInputSpace(Scene):
    def construct(self):
        self.add_plane()
        self.ask_about_direction()
        self.show_gradient()

    def add_plane(self):
        plane = NumberPlane(
            x_radius = FRAME_X_RADIUS/2
        )
        plane.add_coordinates()
        name = TextMobject("Input space")
        name.add_background_rectangle()
        name.next_to(plane.get_corner(UP+LEFT), DOWN+RIGHT)
        x, y = list(map(TexMobject, ["x", "y"]))
        x.next_to(plane.coords_to_point(3.25, 0), UP, SMALL_BUFF)
        y.next_to(plane.coords_to_point(0, 3.6), RIGHT, SMALL_BUFF)

        self.play(
            *list(map(Write, [plane, name, x, y])),
            run_time = 1
        )
        self.wait()

        self.plane = plane

    def ask_about_direction(self):
        point = self.plane.coords_to_point(2, 1)
        dot = Dot(point, color = YELLOW)
        dot.save_state()
        dot.move_to(FRAME_Y_RADIUS*UP + FRAME_X_RADIUS*RIGHT/2)
        dot.fade(1)
        arrows = VGroup(*[
            Arrow(ORIGIN, vect).shift(point)
            for vect in compass_directions(8)
        ])
        arrows.set_color(WHITE)
        question = TextMobject(
            "Which direction decreases \\\\",
            "$C(x, y)$", "most quickly?"
        )
        question.scale(0.7)
        question.set_color(YELLOW)
        question.set_color_by_tex("C(x, y)", RED)
        question.add_background_rectangle()
        question.next_to(arrows, LEFT)

        self.play(dot.restore)
        self.play(
            FadeIn(question),
            LaggedStartMap(GrowArrow, arrows)
        )
        self.wait()

        self.arrows = arrows
        self.dot = dot
        self.question = question

    def show_gradient(self):
        arrows = self.arrows
        dot = self.dot
        question = self.question

        arrow = arrows[3]
        new_arrow = Arrow(
            dot.get_center(), arrow.get_end(),
            buff = 0,
            color = GREEN
        )
        new_arrow.set_color(GREEN)
        arrow.save_state()

        gradient = TexMobject("\\nabla C(x, y)")
        gradient.add_background_rectangle()
        gradient.next_to(arrow.get_end(), UP, SMALL_BUFF)

        gradient_words = TextMobject(
            "``Gradient'', the direction\\\\ of",
            "steepest increase"
        )
        gradient_words.scale(0.7)
        gradient_words[-1].set_color(GREEN)
        gradient_words.next_to(gradient, UP, SMALL_BUFF)
        gradient_words.add_background_rectangle(opacity = 1)
        gradient_words.shift(LEFT)

        anti_arrow = new_arrow.copy()
        anti_arrow.rotate(np.pi, about_point = dot.get_center())
        anti_arrow.set_color(RED)

        self.play(
            Transform(arrow, new_arrow),
            Animation(dot),
            *[FadeOut(a) for a in arrows if a is not arrow]
        )
        self.play(FadeIn(gradient))
        self.play(Write(gradient_words, run_time = 2))
        self.wait(2)
        self.play(
            arrow.fade,
            ReplacementTransform(
                arrow.copy(),
                anti_arrow
            )
        )
        self.wait(2)

class CostSurface(ExternallyAnimatedScene):
    pass

class KhanAcademyMVCWrapper(PiCreatureScene):
    def construct(self):
        screen = ScreenRectangle(height = 5)
        screen.to_corner(UP+LEFT)
        morty = self.pi_creature

        self.play(
            ShowCreation(screen),
            morty.change, "raise_right_hand",
        )
        self.wait(3)
        self.play(morty.change, "happy", screen)
        self.wait(5)

class KAGradientPreview(ExternallyAnimatedScene):
    pass

class GradientDescentAlgorithm(Scene):
    def construct(self):
        words = VGroup(
            TextMobject("Compute", "$\\nabla C$"),
            TextMobject("Small step in", "$-\\nabla C$", "direction"),
            TextMobject("Repeat."),
        )
        words.arrange(DOWN, aligned_edge = LEFT)
        words.set_width(FRAME_WIDTH - 1)
        words.to_corner(DOWN+LEFT)

        for word in words[:2]:
            word[1].set_color(RED)

        for word in words:
            self.play(Write(word, run_time = 1))
            self.wait()

class GradientDescentName(Scene):
    def construct(self):
        words = TextMobject("Gradient descent")
        words.set_color(BLUE)
        words.set_width(FRAME_WIDTH - 1)
        words.to_edge(DOWN)

        self.play(Write(words, run_time = 2))
        self.wait()

class ShowFullCostFunctionGradient(PreviewLearning):
    def construct(self):
        self.organize_weights_as_column_vector()
        self.show_gradient()

    def organize_weights_as_column_vector(self):
        network_mob = self.network_mob
        edges = VGroup(*it.chain(*network_mob.edge_groups))
        layers = VGroup(*network_mob.layers)
        layers.add(network_mob.output_labels)
        self.color_network_edges()

        nums = [2.25, -1.57,  1.98, -1.16,  3.82, 1.21]
        decimals = VGroup(*[
            DecimalNumber(num).set_color(
                BLUE_D if num > 0 else RED
            )
            for num in nums
        ])
        dots = TexMobject("\\vdots")
        decimals.submobjects.insert(3, dots)
        decimals.arrange(DOWN)
        decimals.shift(2*LEFT + 0.5*DOWN)
        lb, rb = brackets = TexMobject("\\big[", "\\big]")
        brackets.scale(2)
        brackets.stretch_to_fit_height(1.2*decimals.get_height())
        lb.next_to(decimals, LEFT, SMALL_BUFF)
        rb.next_to(decimals, RIGHT, SMALL_BUFF)
        column_vect = VGroup(lb, decimals, rb)

        edges_target = VGroup(*it.chain(
            decimals[:3],
            [dots]*(len(edges) - 6),
            decimals[-3:]
        ))

        words = TextMobject("$13{,}002$ weights and biases")
        words.next_to(column_vect, UP)

        lhs = TexMobject("\\vec{\\textbf{W}}", "=")
        lhs[0].set_color(YELLOW)
        lhs.next_to(column_vect, LEFT)

        self.play(
            FadeOut(layers),
            edges.space_out_submobjects, 1.2,
        )
        self.play(
            ReplacementTransform(
                edges, edges_target,
                run_time = 2,
                lag_ratio = 0.5
            ),
            LaggedStartMap(FadeIn, words),
        )
        self.play(*list(map(Write, [lb, rb, lhs])), run_time = 1)
        self.wait()

        self.column_vect = column_vect

    def show_gradient(self):
        column_vect = self.column_vect

        lhs = TexMobject(
            "-", "\\nabla", "C(", "\\vec{\\textbf{W}}", ")", "="
        )
        lhs.shift(2*RIGHT)
        lhs.set_color_by_tex("W", YELLOW)
        old_decimals = VGroup(*[m for m in column_vect[1] if isinstance(m, DecimalNumber)])
        new_decimals = VGroup()
        new_nums = [0.18, 0.45, -0.51, 0.4, -0.32, 0.82]
        for decimal, new_num in zip(old_decimals, new_nums):
            new_decimal = DecimalNumber(new_num)
            new_decimal.set_color(BLUE if new_num > 0 else RED_B)
            new_decimal.move_to(decimal)
            new_decimals.add(new_decimal)
        rhs = VGroup(
            column_vect[0].copy(),
            new_decimals, 
            column_vect[2].copy(),
        )
        rhs.to_edge(RIGHT, buff = 1.75)
        lhs.next_to(rhs, LEFT)

        words = TextMobject("How to nudge all \\\\ weights and biases")
        words.next_to(rhs, UP)

        self.play(Write(VGroup(lhs, rhs)))
        self.play(FadeIn(words))
        for od, nd in zip(old_decimals, new_decimals):
            nd = nd.deepcopy()
            od_num = od.number
            nd_num = nd.number
            self.play(
                nd.move_to, od, 
                nd.shift, 1.5*RIGHT
            )
            self.play(
                Transform(
                    nd, VectorizedPoint(od.get_center()),
                    lag_ratio = 0.5,
                    remover = True
                ),
                ChangingDecimal(
                    od,
                    lambda a : interpolate(od_num, od_num+nd_num, a)
                )
            )
        self.wait()

class DotsInsert(Scene):
    def construct(self):
        dots = TexMobject("\\vdots")
        dots.set_height(FRAME_HEIGHT - 1)
        self.add(dots)

class HowMinimizingCostMeansBetterTrainingPerformance(IntroduceCostFunction):
    def construct(self):
        IntroduceCostFunction.construct(self)
        self.improve_last_layer()

    def improve_last_layer(self):
        decimals = self.decimal_groups[0]
        neurons = self.network_mob.layers[-1].neurons

        values = [d.number for d in decimals]
        target_values = 0.1*np.random.random(10)
        target_values[3] = 0.98

        words = TextMobject("Minimize cost $\\dots$")
        words.next_to(decimals, UP, MED_LARGE_BUFF)
        words.set_color(YELLOW)
        # words.shift(LEFT)

        def generate_update(n1, n2):
            return lambda a : interpolate(n1, n2, a)
        updates = [
            generate_update(n1, n2)
            for n1, n2 in zip(values, target_values)
        ]

        self.play(LaggedStartMap(FadeIn, words, run_time = 1))
        self.play(*[
            ChangingDecimal(d, update)
            for d, update in zip(decimals, updates)
        ] + [
            UpdateFromFunc(
                d,
                lambda mob: mob.set_fill(
                    interpolate_color(BLACK, WHITE, 0.5+0.5*mob.number),
                    opacity = 1
                )
            )
            for d in decimals
        ] + [
            ApplyMethod(neuron.set_fill, WHITE, target_value)
            for neuron, target_value in zip(neurons, target_values)
        ], run_time = 3)
        self.wait()

    ###

    def average_over_all_training_data(self):
        pass #So that IntroduceCostFunction.construct doesn't do this

class CostSurfaceSteps(ExternallyAnimatedScene):
    pass

class ConfusedAboutHighDimension(TeacherStudentsScene):
    def construct(self):
        self.student_says(
            "13{,}002-dimensional \\\\ nudge?",
            target_mode = "confused"
        )
        self.change_student_modes(*["confused"]*3)
        self.wait(2)
        self.teacher_thinks(
            "",
            bubble_kwargs = {"width" : 6, "height" : 4},
            added_anims = [self.get_student_changes(*["plain"]*3)]
        )
        self.zoom_in_on_thought_bubble()

class NonSpatialGradientIntuition(Scene):
    CONFIG = {
        "w_color" : YELLOW,
        "positive_color" : BLUE,
        "negative_color" : RED,
        "vect_height" : FRAME_Y_RADIUS - MED_LARGE_BUFF,
        "text_scale_value" : 0.7,
    }
    def construct(self):
        self.add_vector()
        self.add_gradient()
        self.show_sign_interpretation()
        self.show_magnitude_interpretation()

    def add_vector(self):
        lhs = TexMobject("\\vec{\\textbf{W}}", "=")
        lhs[0].set_color(self.w_color)
        lhs.to_edge(LEFT)

        ws = VGroup(*[
            VGroup(TexMobject(tex))
            for tex in it.chain(
                ["w_%d"%d for d in range(3)],
                ["\\vdots"],
                ["w_{13{,}00%d}"%d for d in range(3)]
            )
        ])
        ws.set_color(self.w_color)
        ws.arrange(DOWN)
        lb, rb = brackets = TexMobject("\\big[", "\\big]").scale(2)
        brackets.stretch_to_fit_height(1.2*ws.get_height())
        lb.next_to(ws, LEFT)
        rb.next_to(ws, RIGHT)
        vect = VGroup(lb, ws, rb)

        vect.set_height(self.vect_height)
        vect.to_edge(UP).shift(2*LEFT)
        lhs.next_to(vect, LEFT)

        self.add(lhs, vect)
        self.vect = vect
        self.top_lhs = lhs

    def add_gradient(self):
        lb, ws, rb = vect = self.vect
        ws = VGroup(*ws)
        dots = ws[len(ws)/2]
        ws.remove(dots)

        lhs = TexMobject(
            "-\\nabla", "C(", "\\vec{\\textbf{W}}", ")", "="
        )
        lhs.next_to(vect, RIGHT, LARGE_BUFF)
        lhs.set_color_by_tex("W", self.w_color)

        decimals = VGroup()
        nums = [0.31, 0.03, -1.25, 0.78, -0.37, 0.16]
        for num, w in zip(nums, ws):
            decimal = DecimalNumber(num)
            decimal.scale(self.text_scale_value)
            if num > 0:
                decimal.set_color(self.positive_color)
            else:
                decimal.set_color(self.negative_color)
            decimal.move_to(w)
            decimals.add(decimal)
        new_dots = dots.copy()

        grad_content = VGroup(*it.chain(
            decimals[:3], new_dots, decimals[3:]
        ))
        grad_vect = VGroup(lb.copy(), grad_content, rb.copy())
        VGroup(grad_vect[0], grad_vect[-1]).space_out_submobjects(0.8)
        grad_vect.set_height(self.vect_height)
        grad_vect.next_to(self.vect, DOWN)
        lhs.next_to(grad_vect, LEFT)

        brace = Brace(grad_vect, RIGHT)
        words = brace.get_text("Example gradient")

        self.wait()
        self.play(
            ReplacementTransform(self.top_lhs.copy(), lhs),
            ReplacementTransform(self.vect.copy(), grad_vect),
            GrowFromCenter(brace),
            FadeIn(words)
        )
        self.wait()
        self.play(FadeOut(VGroup(brace, words)))

        self.ws = ws
        self.grad_lhs = lhs
        self.grad_vect = grad_vect
        self.decimals = decimals

    def show_sign_interpretation(self):
        ws = self.ws.copy()
        decimals = self.decimals

        direction_phrases = VGroup()
        for w, decimal in zip(ws, decimals):
            if decimal.number > 0:
                verb = "increase"
                color = self.positive_color
            else:
                verb = "decrease"
                color = self.negative_color
            phrase = TextMobject("should", verb)
            phrase.scale(self.text_scale_value)
            phrase.set_color_by_tex(verb, color)
            w.generate_target()
            group = VGroup(w.target, phrase)
            group.arrange(RIGHT)
            w.target.shift(0.7*SMALL_BUFF*DOWN)
            group.move_to(decimal.get_center() + RIGHT, LEFT)
            direction_phrases.add(phrase)

        self.play(
            LaggedStartMap(MoveToTarget, ws),
            LaggedStartMap(FadeIn, direction_phrases)
        )
        self.wait(2)

        self.direction_phrases = direction_phrases
        self.ws = ws

    def show_magnitude_interpretation(self):
        direction_phrases = self.direction_phrases
        ws = self.ws
        decimals = self.decimals

        magnitude_words = VGroup()
        rects = VGroup()
        for phrase, decimal in zip(direction_phrases, decimals):
            if abs(decimal.number) < 0.2:
                adj = "a little"
                color = interpolate_color(BLACK, WHITE, 0.5)
            elif abs(decimal.number) < 0.5:
                adj = "somewhat"
                color = LIGHT_GREY
            else:
                adj = "a lot"
                color =  WHITE
            words = TextMobject(adj)
            words.scale(self.text_scale_value)
            words.set_color(color)
            words.next_to(phrase, RIGHT, SMALL_BUFF)
            magnitude_words.add(words)

            rect = SurroundingRectangle(
                VGroup(*decimal[-4:]), 
                buff = SMALL_BUFF,
                color = LIGHT_GREY
            )
            rect.target = words
            rects.add(rect)

        self.play(LaggedStartMap(ShowCreation, rects))
        self.play(LaggedStartMap(MoveToTarget, rects))
        self.wait(2)

class SomeConnectionsMatterMoreThanOthers(PreviewLearning):
    def setup(self):
        np.random.seed(1)
        PreviewLearning.setup(self)
        self.color_network_edges()

        ex_in = get_organized_images()[3][4]
        image = MNistMobject(ex_in)
        image.to_corner(UP+LEFT)
        self.add(image)
        self.ex_in = ex_in

    def construct(self):
        self.activate_network(self.ex_in)
        self.fade_edges()
        self.show_important_connection()
        self.show_unimportant_connection()

    def fade_edges(self):
        edges = VGroup(*it.chain(*self.network_mob.edge_groups))
        self.play(*[
            ApplyMethod(
                edge.set_stroke, BLACK, 0,
                rate_func = lambda a : 0.5*smooth(a)
            )
            for edge in edges
        ])

    def show_important_connection(self):
        layers = self.network_mob.layers
        edge = self.get_edge(2, 3)
        edge.set_stroke(YELLOW, 4)
        words = TextMobject("This weight \\\\ matters a lot")
        words.next_to(layers[-1], UP).to_edge(UP)
        words.set_color(YELLOW)
        arrow = Arrow(words.get_bottom(), edge.get_center())

        self.play(
            ShowCreation(edge),
            GrowArrow(arrow),
            FadeIn(words)
        )
        self.wait()

    def show_unimportant_connection(self):
        color = TEAL
        edge = self.get_edge(11, 6)
        edge.set_stroke(color, 5)
        words = TextMobject("Who even cares \\\\ about this weight?")
        words.next_to(self.network_mob.layers[-1], DOWN)
        words.to_edge(DOWN)
        words.set_color(color)
        arrow = Arrow(words.get_top(), edge.get_center(), buff = SMALL_BUFF)
        arrow.set_color(color)

        self.play(
            ShowCreation(edge),
            GrowArrow(arrow),
            FadeIn(words)
        )
        self.wait()
    ###

    def get_edge(self, i1, i2):
        layers = self.network_mob.layers
        n1 = layers[-2].neurons[i1]
        n2 = layers[-1].neurons[i2]
        return self.network_mob.get_edge(n1, n2)

class SpinningVectorWithLabel(Scene):
    def construct(self):
        plane = NumberPlane(
            x_unit_size = 2,
            y_unit_size = 2,
        )
        plane.add_coordinates()
        self.add(plane)

        vector = Vector(2*RIGHT)
        label = get_decimal_vector([-1, -1], with_dots = False)
        label.add_to_back(BackgroundRectangle(label))
        label.next_to(vector.get_end(), UP+RIGHT)
        label.decimals.set_fill(opacity = 0)
        decimals = label.decimals.copy()
        decimals.set_fill(WHITE, 1)

        cd1 = ChangingDecimal(
            decimals[0],
            lambda a : np.cos(vector.get_angle()),
            tracked_mobject = label.decimals[0],
        )
        cd2 = ChangingDecimal(
            decimals[1],
            lambda a : np.sin(vector.get_angle()),
            tracked_mobject = label.decimals[1],
        )

        self.play(
            Rotate(
                vector, 
                0.999*np.pi,
                in_place = False,
                run_time = 8,
                rate_func = there_and_back
            ),
            UpdateFromFunc(
                label,
                lambda m : m.next_to(vector.get_end(), UP+RIGHT)
            ),
            cd1, cd2,
        )
        self.wait()

class TwoGradientInterpretationsIn2D(Scene):
    def construct(self):
        self.force_skipping()

        self.setup_plane()
        self.add_function_definitions()
        self.point_out_direction()
        self.point_out_relative_importance()
        self.wiggle_in_neighborhood()

    def setup_plane(self):
        plane = NumberPlane()
        plane.add_coordinates()
        self.add(plane)
        self.plane = plane

    def add_function_definitions(self):
        func = TexMobject(
            "C(", "x, y", ")", "=", 
            "\\frac{3}{2}x^2", "+", "\\frac{1}{2}y^2",
        )
        func.shift(FRAME_X_RADIUS*LEFT/2).to_edge(UP)

        grad = TexMobject("\\nabla", "C(", "1, 1", ")", "=")
        vect = TexMobject(
            "\\left[\\begin{array}{c} 3 \\\\ 1 \\end{array}\\right]"
        )
        vect.next_to(grad, RIGHT, SMALL_BUFF)
        grad_group = VGroup(grad, vect)
        grad_group.next_to(ORIGIN, RIGHT).to_edge(UP, buff = MED_SMALL_BUFF)
        for mob in grad, vect, func:
            mob.add_background_rectangle()
            mob.background_rectangle.scale_in_place(1.1)

        self.play(Write(func, run_time = 1))
        self.play(Write(grad_group, run_time = 2))
        self.wait()

        self.func = func
        self.grad = grad
        self.vect = vect

    def point_out_direction(self):
        coords = self.grad.get_part_by_tex("1, 1").copy()
        vect = self.vect[1].copy()
        coords.set_color(YELLOW)
        vect.set_color(GREEN)

        dot = Dot(self.plane.coords_to_point(1, 1))
        dot.set_color(coords.get_color())
        arrow = Arrow(
            self.plane.coords_to_point(1, 1),
            self.plane.coords_to_point(4, 2),
            buff = 0,
            color = vect.get_color()
        )
        words = TextMobject("Direction of \\\\ steepest ascent")
        words.add_background_rectangle()
        words.next_to(ORIGIN, DOWN)
        words.rotate(arrow.get_angle())
        words.shift(arrow.get_center())

        self.play(DrawBorderThenFill(coords, run_time = 1))
        self.play(ReplacementTransform(coords.copy(), dot))
        self.play(DrawBorderThenFill(vect, run_time = 1))
        self.play(
            ReplacementTransform(vect.copy(), arrow),
            Animation(dot)
        )
        self.play(Write(words))
        self.wait()

        self.remove(vect)
        self.vect[1].set_color(vect.get_color())
        self.remove(coords)
        self.grad.get_part_by_tex("1, 1").set_color(coords.get_color())

        self.steepest_words = words
        self.dot = dot

    def point_out_relative_importance(self):
        func = self.func
        grad_group = VGroup(self.grad, self.vect)
        x_part = func.get_part_by_tex("x^2")
        y_part = func.get_part_by_tex("y^2")

        self.play(func.shift, 1.5*DOWN)

        x_rect = SurroundingRectangle(x_part, color = YELLOW)
        y_rect = SurroundingRectangle(y_part, color = TEAL)
        x_words = TextMobject("$x$ has 3 times \\\\ the impact...")
        x_words.set_color(x_rect.get_color())
        x_words.add_background_rectangle()
        x_words.next_to(x_rect, UP)
        # x_words.to_edge(LEFT)
        y_words = TextMobject("...as $y$")
        y_words.set_color(y_rect.get_color())
        y_words.add_background_rectangle()
        y_words.next_to(y_rect, DOWN)

        self.play(
            Write(x_words, run_time = 2),
            ShowCreation(x_rect)
        )
        self.wait()
        self.play(
            Write(y_words, run_time = 1),
            ShowCreation(y_rect)
        )
        self.wait(2)

    def wiggle_in_neighborhood(self):
        dot = self.dot
        steepest_words = self.steepest_words

        neighborhood = Circle(
            fill_color = BLUE,
            fill_opacity = 0.25,
            stroke_width = 0,
            radius = 0.5,
        )
        neighborhood.move_to(dot)

        self.revert_to_original_skipping_status()
        self.play(
            FadeOut(steepest_words),
            GrowFromCenter(neighborhood)
        )
        self.wait()
        for vect in RIGHT, UP, 0.3*(3*RIGHT + UP):
            self.play(
                dot.shift, 0.5*vect,
                rate_func = lambda t : wiggle(t, 4),
                run_time = 3,
            )
            self.wait()

class ParaboloidGraph(ExternallyAnimatedScene):
    pass

class TODOInsertEmphasizeComplexityOfCostFunctionCopy(TODOStub):
    CONFIG = {
        "message" : "Insert EmphasizeComplexityOfCostFunction copy"
    }

class GradientNudging(PreviewLearning):
    CONFIG = {
        "n_steps" : 10,
        "n_decimals" : 8,
    }
    def construct(self):
        self.setup_network_mob()
        self.add_gradient()
        self.change_weights_repeatedly()

    def setup_network_mob(self):
        network_mob = self.network_mob
        self.color_network_edges()
        network_mob.scale(0.7)
        network_mob.to_corner(DOWN+RIGHT)

    def add_gradient(self):
        lhs = TexMobject(
            "-", "\\nabla", "C(", "\\dots", ")", "="
        )
        brace = Brace(lhs.get_part_by_tex("dots"), DOWN)
        words = brace.get_text("All weights \\\\ and biases")
        words.scale(0.8, about_point = words.get_top())
        np.random.seed(3)
        nums = 4*(np.random.random(self.n_decimals)-0.5)
        vect = get_decimal_vector(nums)
        vect.next_to(lhs, RIGHT)
        group = VGroup(lhs, brace, words, vect)
        group.to_corner(UP+LEFT)

        self.add(*group)

        self.set_variables_as_attrs(
            grad_lhs = lhs,
            grad_vect = vect,
            grad_arg_words = words,
            grad_arg_brace = brace
        )

    def change_weights_repeatedly(self):
        network_mob = self.network_mob
        edges = VGroup(*reversed(list(
            it.chain(*network_mob.edge_groups)
        )))

        decimals = self.grad_vect.decimals

        words = TextMobject(
            "Change by some small\\\\",
            "multiple of $-\\nabla C(\\dots)$"
        )
        words.next_to(network_mob, UP).to_edge(UP)
        arrows = VGroup(*[
            Arrow(
                words.get_bottom(),
                edge_group.get_top(),
                color = WHITE
            )
            for edge_group in network_mob.edge_groups
        ])

        mover = VGroup(*decimals.family_members_with_points()).copy()
        # mover.set_fill(opacity = 0)
        mover.set_stroke(width = 0)
        target = VGroup(*self.network_mob.edge_groups.family_members_with_points()).copy()
        target.set_fill(opacity = 0)
        ApplyMethod(target.set_stroke, YELLOW, 2).update(0.3)
        self.play(
            ReplacementTransform(mover, target),
            FadeIn(words),
            LaggedStartMap(GrowArrow, arrows, run_time = 1)
        )
        self.play(FadeOut(target))
        self.play(self.get_edge_change_anim(edges))
        self.play(*self.get_decimal_change_anims(decimals))
        for x in range(self.n_steps):
            self.play(self.get_edge_change_anim(edges))
            self.play(*self.get_decimal_change_anims(decimals))
        self.wait()

    ###

    def get_edge_change_anim(self, edges):
        target_nums = 6*(np.random.random(len(edges))-0.5)
        edges.generate_target()
        for edge, target_num in zip(edges.target, target_nums):
            curr_num = edge.get_stroke_width()
            if Color(edge.get_stroke_color()) == Color(self.negative_edge_color):
                curr_num *= -1
            new_num = interpolate(curr_num, target_num, 0.2)
            if new_num > 0:
                new_color = self.positive_edge_color
            else:
                new_color = self.negative_edge_color
            edge.set_stroke(new_color, abs(new_num))
            edge.rotate_in_place(np.pi)
        return MoveToTarget(
            edges,
            lag_ratio = 0.5,
            run_time = 1.5
        )

    def get_decimal_change_anims(self, decimals):
        words = TextMobject("Recompute \\\\ gradient")
        words.next_to(decimals, DOWN, MED_LARGE_BUFF)
        def wrf(t):
            if t < 1./3:
                return smooth(3*t)
            elif t < 2./3:
                return 1
            else:
                return smooth(3 - 3*t)

        changes = 0.2*(np.random.random(len(decimals))-0.5)
        def generate_change_func(x, dx):
            return lambda a : interpolate(x, x+dx, a)
        return [
            ChangingDecimal(
                decimal, 
                generate_change_func(decimal.number, change)
            )
            for decimal, change in zip(decimals, changes)
        ] + [
            FadeIn(words, rate_func = wrf, run_time = 1.5, remover = True)
        ]

class BackPropWrapper(PiCreatureScene):
    def construct(self):
        morty = self.pi_creature
        screen = ScreenRectangle(height = 5)
        screen.to_corner(UP+LEFT)
        screen.shift(MED_LARGE_BUFF*DOWN)

        title = TextMobject("Backpropagation", "(next video)")
        title.next_to(screen, UP)

        self.play(
            morty.change, "raise_right_hand", screen,
            ShowCreation(screen)
        )
        self.play(Write(title[0], run_time = 1))
        self.wait()
        self.play(Write(title[1], run_time = 1))
        self.play(morty.change, "happy", screen)
        self.wait(5)

class TODOInsertCostSurfaceSteps(TODOStub):
    CONFIG = {
        "message" : "Insert CostSurfaceSteps"
    }

class ContinuouslyRangingNeuron(PreviewLearning):
    def construct(self):
        self.color_network_edges()
        network_mob = self.network_mob
        network_mob.scale(0.8)
        network_mob.to_edge(DOWN)
        neuron = self.network_mob.layers[2].neurons[6]
        decimal = DecimalNumber(0)
        decimal.set_width(0.8*neuron.get_width())
        decimal.move_to(neuron)

        decimal.generate_target()
        neuron.generate_target()
        group = VGroup(neuron.target, decimal.target)
        group.set_height(1)
        group.next_to(network_mob, UP)
        decimal.set_fill(opacity = 0)

        def update_decimal_color(decimal):
            if neuron.get_fill_opacity() > 0.8:
                decimal.set_color(BLACK)
            else:
                decimal.set_color(WHITE)
        decimal_color_anim = UpdateFromFunc(decimal, update_decimal_color)

        self.play(*list(map(MoveToTarget, [neuron, decimal])))
        for x in 0.7, 0.35, 0.97, 0.23, 0.54:
            curr_num = neuron.get_fill_opacity()
            self.play(
                neuron.set_fill, None, x,
                ChangingDecimal(
                    decimal, lambda a : interpolate(curr_num, x, a)
                ),
                decimal_color_anim
            )
            self.wait()

class AskHowItDoes(TeacherStudentsScene):
    def construct(self):
        self.student_says(
            "How well \\\\ does it do?",
            student_index = 0
        )
        self.wait(5)

class TestPerformance(PreviewLearning):
    CONFIG = {
        "n_examples" : 300,
        "time_per_example" : 0.1,
        "wrong_wait_time" : 0.5,
        "stroke_width_exp" : 2,
        "decimal_kwargs" : {
            "num_decimal_places" : 3,
        }
    }
    def construct(self):
        self.setup_network_mob()
        self.init_testing_data()
        self.add_title()
        self.add_fraction()
        self.run_through_examples()

    def setup_network_mob(self):
        self.network_mob.set_height(5)
        self.network_mob.to_corner(DOWN+LEFT)
        self.network_mob.to_edge(DOWN, buff = MED_SMALL_BUFF)

    def init_testing_data(self):
        training_data, validation_data, test_data = load_data_wrapper()
        self.test_data = iter(test_data[:self.n_examples])

    def add_title(self):
        title = TextMobject("Testing data")
        title.to_edge(UP, buff = MED_SMALL_BUFF)
        title.to_edge(LEFT, buff = LARGE_BUFF)
        self.add(title)
        self.title = title

    def add_fraction(self):
        self.n_correct = 0
        self.total = 0
        self.decimal = DecimalNumber(0, **self.decimal_kwargs)
        word_frac = TexMobject(
            "{\\text{Number correct}", "\\over", 
            "\\text{total}}", "=",
        )
        word_frac[0].set_color(GREEN)
        self.frac = self.get_frac()
        self.equals = TexMobject("=")
        fracs = VGroup(
            word_frac, self.frac, 
            self.equals, self.decimal
        )
        fracs.arrange(RIGHT)
        fracs.to_corner(UP+RIGHT, buff = LARGE_BUFF)
        self.add(fracs)

    def run_through_examples(self):
        title = self.title
        rects = [
            SurroundingRectangle(
                VGroup(neuron, label),
                buff = 0.5*SMALL_BUFF
            )
            for neuron, label in zip(
                self.network_mob.layers[-1].neurons,
                self.network_mob.output_labels
            )
        ]
        rect_wrong = TextMobject("Wrong!")
        rect_wrong.set_color(RED)
        num_wrong = rect_wrong.copy()

        arrow = Arrow(LEFT, RIGHT, color = WHITE)
        guess_word = TextMobject("Guess")
        self.add(arrow, guess_word)

        from tqdm import tqdm as ProgressDisplay
        for test_in, test_out in ProgressDisplay(list(self.test_data)):
            self.total += 1

            activations = self.activate_layers(test_in)
            choice = np.argmax(activations[-1])

            image = MNistMobject(test_in)
            image.set_height(1.5)
            choice_mob = TexMobject(str(choice))
            choice_mob.scale(1.5)
            group = VGroup(image, arrow, choice_mob)
            group.arrange(RIGHT)
            group.shift(
                self.title.get_bottom()+MED_SMALL_BUFF*DOWN -\
                image.get_top()
            )
            self.add(image, choice_mob)
            guess_word.next_to(arrow, UP, SMALL_BUFF)

            rect = rects[choice]
            self.add(rect)

            correct = (choice == test_out)
            if correct:
                self.n_correct += 1
            else:
                rect_wrong.next_to(rect, RIGHT)
                num_wrong.next_to(choice_mob, DOWN)
                self.add(rect_wrong, num_wrong)
            new_frac = self.get_frac()
            new_frac.shift(
                self.frac[1].get_left() - \
                new_frac[1].get_left()
            )
            self.remove(self.frac)
            self.add(new_frac)
            self.frac = new_frac
            self.equals.next_to(new_frac, RIGHT)

            new_decimal = DecimalNumber(
                float(self.n_correct)/self.total,
                **self.decimal_kwargs
            )
            new_decimal.next_to(self.equals, RIGHT)
            self.remove(self.decimal)
            self.add(new_decimal)
            self.decimal = new_decimal

            self.wait(self.time_per_example)
            if not correct:
                self.wait(self.wrong_wait_time)

            self.remove(rect, rect_wrong, num_wrong, image, choice_mob)

        self.add(rect, image, choice_mob)

    ###
    def add_network(self):
        self.network_mob = MNistNetworkMobject(**self.network_mob_config)
        self.network_mob.scale(0.8)
        self.network_mob.to_edge(DOWN)
        self.network = self.network_mob.neural_network
        self.add(self.network_mob)
        self.color_network_edges()

    def get_frac(self):
        frac = TexMobject("{%d"%self.n_correct, "\\over", "%d}"%self.total)
        frac[0].set_color(GREEN)
        return frac

    def activate_layers(self, test_in):
        activations = self.network.get_activation_of_all_layers(test_in)
        layers = self.network_mob.layers
        for layer, activation in zip(layers, activations)[1:]:
            for neuron, a in zip(layer.neurons, activation):
                neuron.set_fill(opacity = a)
        return activations

class ReactToPerformance(TeacherStudentsScene):
    def construct(self):
        title = VGroup(
            TextMobject("Play with network structure"),
            Arrow(LEFT, RIGHT, color = WHITE),
            TextMobject("98\\%", "testing accuracy")
        )
        title.arrange(RIGHT)
        title.to_edge(UP)
        title[-1][0].set_color(GREEN)
        self.play(Write(title, run_time = 2))

        last_words = TextMobject(
            "State of the art \\\\ is", 
            "99.79\\%"
        )
        last_words[-1].set_color(GREEN)

        self.teacher_says(
            "That's pretty", "good!",
            target_mode = "surprised",
            run_time = 1
        )
        self.change_student_modes(*["hooray"]*3)
        self.wait()
        self.teacher_says(last_words, target_mode = "hesitant")
        self.change_student_modes(
            *["pondering"]*3,
            look_at_arg = self.teacher.bubble
        )
        self.wait()

class NoticeWhereItMessesUp(TeacherStudentsScene):
    def construct(self):
        self.teacher_says(
            "Look where it \\\\ messes up",
            run_time = 1
        )
        self.wait(2)

class WrongExamples(TestPerformance):
    CONFIG = {
        "time_per_example" : 0
    }

class TODOBreakUpNineByPatterns(TODOStub):
    CONFIG = {
        "message" : "Insert the scene with 9 \\\\ broken up by patterns"
    }

class NotAtAll(TeacherStudentsScene, PreviewLearning):
    def setup(self):
        TeacherStudentsScene.setup(self)
        PreviewLearning.setup(self)

    def construct(self):
        words = TextMobject("Well...\\\\", "not at all!")
        words[1].set_color(BLACK)
        network_mob = self.network_mob
        network_mob.set_height(4)
        network_mob.to_corner(UP+LEFT)
        self.add(network_mob)
        self.color_network_edges()

        self.teacher_says(
            words, target_mode = "guilty",
            run_time = 1
        )
        self.change_student_modes(*["sassy"]*3)
        self.play(
            self.teacher.change, "concerned_musician",
            words[1].set_color, WHITE
        )
        self.wait(2)

class InterpretFirstWeightMatrixRows(TestPerformance):
    CONFIG = {
        "stroke_width_exp" : 1,
    }
    def construct(self):
        self.slide_network_to_side()
        self.prepare_pixel_arrays()
        self.show_all_pixel_array()

    def slide_network_to_side(self):
        network_mob = self.network_mob
        network_mob.generate_target()
        to_fade = VGroup(*it.chain(
            network_mob.edge_groups[1:],
            network_mob.layers[2:],
            network_mob.output_labels
        ))
        to_keep = VGroup(*it.chain(
            network_mob.edge_groups[0],
            network_mob.layers[:2]
        ))
        shift_val = FRAME_X_RADIUS*LEFT + MED_LARGE_BUFF*RIGHT - \
                    to_keep.get_left()
        self.play(
            to_fade.shift, shift_val,
            to_fade.fade, 1,
            to_keep.shift, shift_val
        )
        self.remove(to_fade)

    def prepare_pixel_arrays(self):
        pixel_arrays = VGroup()
        w_matrix = self.network.weights[0]
        for row in w_matrix:
            max_val = np.max(np.abs(row))
            shades = np.array(row)/max_val
            pixel_array = PixelsFromVect(np.zeros(row.size))
            for pixel, shade in zip(pixel_array, shades):
                if shade > 0:
                    color = self.positive_edge_color
                else:
                    color = self.negative_edge_color
                pixel.set_fill(color, opacity = abs(shade)**(0.3))
            pixel_arrays.add(pixel_array)
        pixel_arrays.arrange_in_grid(buff = MED_LARGE_BUFF)
        pixel_arrays.set_height(FRAME_HEIGHT - 2.5)
        pixel_arrays.to_corner(DOWN+RIGHT)

        for pixel_array in pixel_arrays:
            rect = SurroundingRectangle(pixel_array)
            rect.set_color(WHITE)
            pixel_array.rect = rect

        words = TextMobject("What second layer \\\\ neurons look for")
        words.next_to(pixel_arrays, UP).to_edge(UP)

        self.pixel_arrays = pixel_arrays
        self.words = words

    def show_all_pixel_array(self):
        edges = self.network_mob.edge_groups[0]
        neurons = self.network_mob.layers[1].neurons
        edges.remove(neurons[0].edges_in)

        self.play(
            VGroup(*neurons[1:]).set_stroke, None, 0.5,
            FadeIn(self.words),
            neurons[0].edges_in.set_stroke, None, 2,
            *[
                ApplyMethod(edge.set_stroke, None, 0.25)
                for edge in edges
                if edge not in neurons[0].edges_in
            ]
        )
        self.wait()
        last_neuron = None

        for neuron, pixel_array in zip(neurons, self.pixel_arrays):
            if last_neuron:
                self.play(
                    last_neuron.edges_in.set_stroke, None, 0.25,
                    last_neuron.set_stroke, None, 0.5,
                    neuron.set_stroke, None, 3,
                    neuron.edges_in.set_stroke, None, 2,
                )
            self.play(ReplacementTransform(
                neuron.edges_in.copy().set_fill(opacity = 0),
                pixel_array,
            ))
            self.play(ShowCreation(pixel_array.rect))
            last_neuron = neuron

class InputRandomData(TestPerformance):
    def construct(self):
        self.color_network_edges()
        self.show_random_image()
        self.show_expected_outcomes()
        self.feed_in_random_data()
        self.network_speaks()

    def show_random_image(self):
        np.random.seed(4)
        rand_vect = np.random.random(28*28)
        image = PixelsFromVect(rand_vect)
        image.to_edge(LEFT)
        image.shift(UP)
        rect = SurroundingRectangle(image)

        arrow = Arrow(
            rect.get_top(),
            self.network_mob.layers[0].neurons.get_top(),
            path_arc = -2*np.pi/3,
        )
        arrow.tip.set_stroke(width = 3)

        self.play(
            ShowCreation(rect),
            LaggedStartMap(
                DrawBorderThenFill, image, 
                stroke_width = 0.5
            )
        )
        self.play(ShowCreation(arrow))
        self.wait()

        self.image = image
        self.rand_vect = rand_vect
        self.image_rect = rect
        self.arrow = arrow

    def show_expected_outcomes(self):
        neurons = self.network_mob.layers[-1].neurons

        words = TextMobject("What might you expect?")
        words.to_corner(UP+RIGHT)
        arrow = Arrow(
            words.get_bottom(), neurons.get_top(),
            color = WHITE
        )

        self.play(
            Write(words, run_time = 1),
            GrowArrow(arrow)
        )
        vects = [np.random.random(10) for x in range(2)]
        vects += [np.zeros(10), 0.4*np.ones(10)]
        for vect in vects:
            neurons.generate_target()
            for neuron, o in zip(neurons, vect):
                neuron.generate_target()
                neuron.target.set_fill(WHITE, opacity = o)
            self.play(LaggedStartMap(
                MoveToTarget, neurons,
                run_time = 1
            ))
            self.wait()
        self.play(FadeOut(VGroup(words, arrow)))

    def feed_in_random_data(self):
        neurons = self.network_mob.layers[0].neurons
        rand_vect = self.rand_vect
        image = self.image.copy()
        output_labels = self.network_mob.output_labels

        opacities = it.chain(rand_vect[:8], rand_vect[-8:])
        target_neurons = neurons.copy()
        for n, o in zip(target_neurons, opacities):
            n.set_fill(WHITE, opacity = o)

        point = VectorizedPoint(neurons.get_center())
        image.target = VGroup(*it.chain(
            target_neurons[:len(neurons)/2],
            [point]*(len(image) - len(neurons)),
            target_neurons[-len(neurons)/2:]
        ))

        self.play(MoveToTarget(
            image, 
            run_time = 2,
            lag_ratio = 0.5
        ))
        self.activate_network(rand_vect, FadeOut(image))

        ### React ###
        neurons = self.network_mob.layers[-1].neurons
        choice = np.argmax([n.get_fill_opacity() for n in neurons])
        rect = SurroundingRectangle(VGroup(
            neurons[choice], output_labels[choice]
        ))
        word = TextMobject("What?!?")
        word.set_color(YELLOW)
        word.next_to(rect, RIGHT)

        self.play(ShowCreation(rect))
        self.play(Write(word, run_time = 1))
        self.wait()

        self.network_mob.add(rect, word)
        self.choice = choice

    def network_speaks(self):
        network_mob = self.network_mob
        network_mob.generate_target(use_deepcopy = True)
        network_mob.target.scale(0.7)
        network_mob.target.to_edge(DOWN)
        eyes = Eyes(
            network_mob.target.edge_groups[1],
            height = 0.45,
        )
        eyes.shift(0.5*SMALL_BUFF*UP)

        bubble = SpeechBubble(
            height = 3, width = 5,
            direction = LEFT
        )
        bubble.pin_to(network_mob.target.edge_groups[-1])
        bubble.write("Looks like a \\\\ %d to me!"%self.choice)

        self.play(
            MoveToTarget(network_mob),
            FadeIn(eyes)
        )
        self.play(eyes.look_at_anim(self.image))
        self.play(
            ShowCreation(bubble),
            Write(bubble.content, run_time = 1)
        )
        self.play(eyes.blink_anim())
        self.wait()

class CannotDraw(PreviewLearning):
    def construct(self):
        network_mob = self.network_mob
        self.color_network_edges()
        network_mob.scale(0.5)
        network_mob.to_corner(DOWN+RIGHT)
        eyes = Eyes(network_mob.edge_groups[1])
        eyes.shift(SMALL_BUFF*UP)
        self.add(eyes)

        bubble = SpeechBubble(
            height = 3, width = 4,
            direction = RIGHT
        )
        bubble.pin_to(network_mob.edge_groups[0])
        bubble.write("Uh...I'm really \\\\ more of a multiple \\\\ choice guy")

        randy = Randolph()
        randy.to_corner(DOWN+LEFT)
        eyes.look_at_anim(randy.eyes).update(1)

        self.play(PiCreatureSays(
            randy, "Draw a \\\\ 5 for me",
            look_at_arg = eyes,
            run_time = 1
        ))
        self.play(eyes.change_mode_anim("concerned_musician"))
        self.play(
            ShowCreation(bubble),
            Write(bubble.content),
            eyes.look_at_anim(network_mob.get_corner(DOWN+RIGHT))
        )
        self.play(eyes.blink_anim())
        self.play(Blink(randy))
        self.wait()

class TODOShowCostFunctionDef(TODOStub):
    CONFIG = {
        "message" : "Insert cost function averaging portion"
    }

class TODOBreakUpNineByPatterns2(TODOBreakUpNineByPatterns):
    pass

class SomethingToImproveUpon(PiCreatureScene, TestPerformance):
    CONFIG = {
        "n_examples" : 15,
        "time_per_example" : 0.15,
    }
    def setup(self):
        self.setup_bases()
        self.color_network_edges()
        self.network_mob.to_edge(LEFT)
        edge_update = ContinualEdgeUpdate(
            self.network_mob,
            colors = [BLUE, BLUE, RED]
        )
        edge_update.internal_time = 1
        self.add(edge_update)

    def construct(self):
        self.show_path()
        self.old_news()
        self.recognizing_digits()
        self.hidden_layers()

    def show_path(self):
        network_mob = self.network_mob
        morty = self.pi_creature

        line = Line(LEFT, RIGHT).scale(5)
        line.shift(UP)
        dots = VGroup(*[
            Dot(line.point_from_proportion(a))
            for a in np.linspace(0, 1, 5)
        ])
        dots.set_color_by_gradient(BLUE, YELLOW)
        path = VGroup(line, dots)
        words = TextMobject("This series")
        words.next_to(line, DOWN)

        self.play(
            network_mob.scale, 0.25,
            network_mob.next_to, path.get_right(), UP,
            ShowCreation(path),
            Write(words, run_time = 1),
            morty.change, "sassy", 
        )
        self.wait(2)
        self.play(
            ApplyMethod(
                network_mob.next_to, path.get_left(), UP,
                path_arc = np.pi/2,
            ),
            Rotate(path, np.pi, in_place = True),
            morty.change, "raise_right_hand"
        )
        self.wait(3)

        self.line = line
        self.path = path
        self.this_series = words

    def old_news(self):
        network_mob = self.network_mob
        morty = self.pi_creature
        line = self.line

        words = TextMobject("Old technology!")
        words.to_edge(UP)
        arrow = Arrow(words.get_left(), network_mob.get_right())

        name = TextMobject("``Multilayer perceptron''")
        name.next_to(words, DOWN)

        cnn = TextMobject("Convolutional NN")
        lstm = TextMobject("LSTM")
        cnn.next_to(line.get_center(), UP)
        lstm.next_to(line.get_right(), UP)

        modern_variants = VGroup(cnn, lstm)
        modern_variants.set_color(YELLOW)

        self.play(
            Write(words, run_time = 1),
            GrowArrow(arrow),
            morty.change_mode, "hesitant"
        )
        self.play(Write(name))
        self.wait()
        self.play(
            FadeIn(modern_variants),
            FadeOut(VGroup(words, arrow, name)),
            morty.change, "thinking"
        )
        self.wait(2)

        self.modern_variants = modern_variants

    def recognizing_digits(self):
        training_data, validation_data, test_data = load_data_wrapper()

        for v_in, choice in validation_data[:self.n_examples]:
            image = MNistMobject(v_in)
            image.set_height(1)
            choice = TexMobject(str(choice))
            choice.scale(2)
            arrow = Vector(RIGHT, color = WHITE)
            group = Group(image, arrow, choice)
            group.arrange(RIGHT)
            group.next_to(self.line, DOWN, LARGE_BUFF)
            group.to_edge(LEFT, buff = LARGE_BUFF)

            self.add(group)
            self.wait(self.time_per_example)
            self.remove(group)

    def hidden_layers(self):
        morty = self.pi_creature
        network_mob = self.network_mob

        self.play(
            network_mob.scale, 4, 
            network_mob.center, 
            network_mob.to_edge, LEFT,
            FadeOut(VGroup(self.path, self.modern_variants, self.this_series)),
            morty.change, "confused",
        )

        hidden_layers = network_mob.layers[1:3]
        rects = VGroup(*list(map(SurroundingRectangle, hidden_layers)))
        np.random.seed(0)

        self.play(ShowCreation(rects))
        self.activate_network(np.random.random(28*28))
        self.wait(3)

class ShiftingFocus(Scene):
    def construct(self):
        how, do, networks, learn = words = TextMobject(
            "How", "do", "neural networks", "learn?"
        )
        networks.set_color(BLUE)
        cross = Cross(networks)
        viewers = TextMobject("video viewers")
        viewers.move_to(networks)
        viewers.set_color(YELLOW)
        cap_do = TextMobject("Do")
        cap_do.move_to(do, DOWN+LEFT)

        self.play(Write(words, run_time = 1))
        self.wait()
        self.play(ShowCreation(cross))
        self.play(
            VGroup(networks, cross).shift, DOWN,
            Write(viewers, run_time = 1)
        )
        self.wait(2)
        self.play(
            FadeOut(how),
            Transform(do, cap_do)
        )
        self.wait(2)

class PauseAndPonder(TeacherStudentsScene):
    def construct(self):
        screen = ScreenRectangle(height = 3.5)
        screen.to_edge(UP+LEFT)

        self.teacher_says(
            "Pause and \\\\ ponder!",
            target_mode = "hooray",
            run_time = 1
        )
        self.play(
            ShowCreation(screen),
            self.get_student_changes(*["pondering"]*3),
        )
        self.wait(6)

class ConvolutionalNetworkPreview(Scene):
    def construct(self):
        vect = get_organized_images()[9][0]
        image = PixelsFromVect(vect)
        image.set_stroke(width = 1)
        image.set_height(FRAME_HEIGHT - 1)
        self.add(image)

        kernels = [
            PixelsFromVect(np.zeros(16))
            for x in range(2)
        ]
        for i, pixel in enumerate(kernels[0]):
            x = i%4
            y = i//4
            if x == y:
                pixel.set_fill(BLUE, 1)
            elif abs(x - y) == 1:
                pixel.set_fill(BLUE, 0.5)
        for i, pixel in enumerate(kernels[1]):
            x = i%4
            if x == 1:
                pixel.set_fill(BLUE, 1)
            elif x == 2:
                pixel.set_fill(RED, 1)
        for kernel in kernels:
            kernel.set_stroke(width = 1)
            kernel.scale(image[0].get_height()/kernel[0].get_height())
            kernel.add(SurroundingRectangle(
                kernel, color = YELLOW, buff = 0
            ))
            self.add(kernel)
            for i, pixel in enumerate(image):
                x = i%28
                y = i//28
                if x > 24 or y > 24:
                    continue
                kernel.move_to(pixel, UP+LEFT)
                self.wait(self.frame_duration)
            self.remove(kernel)

class RandomlyLabeledImageData(Scene):
    CONFIG = {
        "image_label_pairs" : [
            ("lion", "Lion"),
            ("Newton", "Genius"),
            ("Fork", "Fork"),
            ("Trilobite", "Trilobite"),
            ("Puppy", "Puppy"),
            ("Astrolabe", "Astrolabe"),
            ("Adele", "Songbird of \\\\ our generation"),
            ("Cow", "Cow"),
            ("Sculling", "Sculling"),
            ("Pierre_de_Fermat", "Tease"),
        ]
    }
    def construct(self):
        groups = Group()
        labels = VGroup()
        for i, (image_name, label_name) in enumerate(self.image_label_pairs):
            x = i//5
            y = i%5
            group = self.get_training_group(image_name, label_name)
            group.shift(4.5*LEFT + x*FRAME_X_RADIUS*RIGHT)
            group.shift(3*UP + 1.5*y*DOWN)
            groups.add(group)
            labels.add(group[-1])
        permutation = list(range(len(labels)))
        while any(np.arange(len(labels)) == permutation):
            random.shuffle(permutation)
        for label, i in zip(labels, permutation):
            label.generate_target()
            label.target.move_to(labels[i], LEFT)
            label.target.set_color(YELLOW)

        self.play(LaggedStartMap(
            FadeIn, groups,
            run_time = 3,
            lag_ratio = 0.3,
        ))
        self.wait()
        self.play(LaggedStartMap(
            MoveToTarget, labels, 
            run_time = 4,
            lag_ratio = 0.5,
            path_arc = np.pi/3,
        ))
        self.wait()

    def get_training_group(self, image_name, label_name):
        arrow = Vector(RIGHT, color = WHITE)
        image = ImageMobject(image_name)
        image.set_height(1.3)
        image.next_to(arrow, LEFT)
        label = TextMobject(label_name)
        label.next_to(arrow, RIGHT)
        group = Group(image, arrow, label)
        return group

class TrainOnImages(PreviewLearning, RandomlyLabeledImageData):
    CONFIG = {
        "layer_sizes" : [17, 17, 17, 17, 17, 17],
        "network_mob_config" : {
            "brace_for_large_layers" : False,
            "include_output_labels" : False,
        },
    }
    def construct(self):
        self.setup_network_mob()

        image_names, label_names = list(zip(*self.image_label_pairs))
        label_names = list(label_names)
        random.shuffle(label_names)
        groups = [
            self.get_training_group(image_name, label_name)
            for image_name, label_name in zip(image_names, label_names)
        ]


        edges = VGroup(*reversed(list(
            it.chain(*self.network_mob.edge_groups)
        )))

        for group in groups:
            for edge in edges:
                edge.generate_target()
                edge.target.rotate_in_place(np.pi)
                alt_edge = random.choice(edges)
                color = alt_edge.get_stroke_color()
                width = alt_edge.get_stroke_width()
                edge.target.set_stroke(color, width)

            group.to_edge(UP)
            self.add(group)
            self.play(LaggedStartMap(
                MoveToTarget, edges,
                lag_ratio = 0.4,
                run_time = 2,
            ))
            self.remove(group)

    def setup_network_mob(self):
        self.network_mob.set_height(5)
        self.network_mob.to_edge(DOWN)
        self.color_network_edges()

class IntroduceDeepNetwork(Scene):
    def construct(self):
        pass

class AskNetworkAboutMemorizing(YellAtNetwork):
    def construct(self):
        randy = self.pi_creature
        network_mob, eyes = self.get_network_and_eyes()
        eyes.look_at_anim(randy.eyes).update(1)
        self.add(eyes)

        self.pi_creature_says(
            "Are you just \\\\ memorizing?",
            target_mode = "sassy",
            look_at_arg = eyes,
            run_time = 2
        )
        self.wait()
        self.play(eyes.change_mode_anim("sad"))
        self.play(eyes.blink_anim())
        self.wait()

class CompareLearningCurves(GraphScene):
    CONFIG = {
        "x_min" : 0,
        "y_axis_label" : "Value of \\\\ cost function",
        "x_axis_label" : "Number of gradient \\\\ descent steps",
        "graph_origin" : 2*DOWN + 3.5*LEFT,
    }
    def construct(self):
        self.setup_axes()
        self.x_axis_label_mob.to_edge(DOWN)
        self.y_axis_label_mob.to_edge(LEFT)
        self.y_axis_label_mob.set_color(RED)

        slow_decrease = self.get_graph(
            lambda x : 9 - 0.25*x
        )
        faster_decrease = self.get_graph(
            lambda x : 4.3*sigmoid(5*(2-x)) + 3 + 0.5*ReLU(3-x)
        )
        for decrease, p in (slow_decrease, 0.2), (faster_decrease, 0.07):
            y_vals = decrease.get_anchors()[:,1]
            y_vals -= np.cumsum(p*np.random.random(len(y_vals)))
            decrease.make_jagged()
        faster_decrease.move_to(slow_decrease, UP+LEFT)

        slow_label = TextMobject("Randomly-labeled data")
        slow_label.set_color(slow_decrease.get_color())
        slow_label.to_corner(UP+RIGHT)
        slow_line = Line(ORIGIN, RIGHT)
        slow_line.set_stroke(slow_decrease.get_color(), 5)
        slow_line.next_to(slow_label, LEFT)

        fast_label = TextMobject("Properly-labeled data")
        fast_label.set_color(faster_decrease.get_color())
        fast_label.next_to(slow_label, DOWN)
        fast_line = slow_line.copy()
        fast_line.set_color(faster_decrease.get_color())
        fast_line.next_to(fast_label, LEFT)

        self.play(FadeIn(slow_label), ShowCreation(slow_line))
        self.play(ShowCreation(
            slow_decrease,
            run_time = 12,
            rate_func=linear,
        ))
        self.play(FadeIn(fast_label), ShowCreation(fast_line))
        self.play(ShowCreation(
            faster_decrease,
            run_time = 12,
            rate_func=linear,
        ))
        self.wait()

        ####

        line = Line(
            self.coords_to_point(1, 2),
            self.coords_to_point(3, 9),
        )
        rect = Rectangle()
        rect.set_fill(YELLOW, 0.3)
        rect.set_stroke(width = 0)
        rect.replace(line, stretch = True)

        words = TextMobject("Learns structured data more quickly")
        words.set_color(YELLOW)
        words.next_to(rect, DOWN)
        words.add_background_rectangle()

        self.play(DrawBorderThenFill(rect))
        self.play(Write(words))
        self.wait()

class ManyMinimaWords(Scene):
    def construct(self):
        words = TextMobject(
            "Many local minima,\\\\",
            "roughly equal quality"
        )
        words.set_width(FRAME_WIDTH - 1)
        words.to_edge(UP)
        self.play(Write(words))
        self.wait()


class NNPart2PatreonThanks(PatreonThanks):
    CONFIG = {
        "specific_patrons" : [
            "Desmos",
            "Burt Humburg",
            "CrypticSwarm",
            "Juan Benet",
            "Ali Yahya",
            "William",
            "Mayank M. Mehrotra",
            "Lukas Biewald",
            "Samantha D. Suplee",
            "Yana Chernobilsky",
            "Kaustuv DeBiswas",
            "Kathryn Schmiedicke",
            "Yu Jun",
            "Dave Nicponski",
            "Damion Kistler",
            "Markus Persson",
            "Yoni Nazarathy",
            "Ed Kellett",
            "Joseph John Cox",
            "Luc Ritchie",
            "Eric Chow",
            "Mathias Jansson",
            "Pedro Perez Sanchez",
            "David Clark",
            "Michael Gardner",
            "Harsev Singh",
            "Mads Elvheim",
            "Erik Sundell",
            "Xueqi Li",
            "David G. Stork",
            "Tianyu Ge",
            "Ted Suzman",
            "Linh Tran",
            "Andrew Busey",
            "John Haley",
            "Ankalagon",
            "Eric Lavault",
            "Boris Veselinovich",
            "Julian Pulgarin",
            "Jeff Linse",
            "Cooper Jones",
            "Ryan Dahl",
            "Mark Govea",
            "Robert Teed",
            "Jason Hise",
            "Meshal Alshammari",
            "Bernd Sing",
            "James Thornton",
            "Mustafa Mahdi",
            "Mathew Bramson",
            "Jerry Ling",
            "Vecht",
            "Shimin Kuang",
            "Rish Kundalia",
            "Achille Brighton",
            "Ripta Pasay",
        ]
    }






























